feat(i18n): plurals support (#278)

zio/stable
Joaquín Sánchez 2022-12-02 03:18:36 +01:00 committed by GitHub
parent 0f7de38c24
commit c4cf3fb371
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 234 additions and 91 deletions

View File

@ -12,7 +12,7 @@ const loaded = $ref(false)
<img <img
:key="account.avatar" :key="account.avatar"
:src="account.avatar" :src="account.avatar"
:alt="account.username" :alt="$t('account.avatar_description', [account.username])"
loading="lazy" loading="lazy"
rounded-full rounded-full
:class="loaded ? 'bg-gray' : 'bg-gray:10'" :class="loaded ? 'bg-gray' : 'bg-gray:10'"

View File

@ -6,6 +6,8 @@ const { account } = defineProps<{
command?: boolean command?: boolean
}>() }>()
const { t } = useI18n()
const createdAt = $(useFormattedDateTime(() => account.createdAt, { const createdAt = $(useFormattedDateTime(() => account.createdAt, {
month: 'long', month: 'long',
day: 'numeric', day: 'numeric',
@ -43,13 +45,16 @@ function getFieldNameIcon(fieldName: string) {
if (fieldNameIcons[name]) if (fieldNameIcons[name])
return fieldNameIcons[name] return fieldNameIcons[name]
} }
function getFieldIconTitle(fieldName: string) {
return fieldName === 'Joined' ? t('account.joined') : fieldName
}
function previewHeader() { function previewHeader() {
openMediaPreview([{ openMediaPreview([{
id: `${account.acct}:header`, id: `${account.acct}:header`,
type: 'image', type: 'image',
previewUrl: account.header, previewUrl: account.header,
description: `${account.username}'s profile header`, description: t('account.profile_description', [account.username]),
}]) }])
} }
@ -58,7 +63,7 @@ function previewAvatar() {
id: `${account.acct}:avatar`, id: `${account.acct}:avatar`,
type: 'image', type: 'image',
previewUrl: account.avatar, previewUrl: account.avatar,
description: `${account.username}'s avatar`, description: t('account.avatar_description', [account.username]),
}]) }])
} }
@ -86,7 +91,7 @@ watchEffect(() => {
<template> <template>
<div flex flex-col> <div flex flex-col>
<button border="b base" z-1> <button border="b base" z-1>
<img h-50 w-full object-cover :src="account.header" :alt="`${account.username}'s profile header`" @click="previewHeader"> <img h-50 w-full object-cover :src="account.header" :alt="t('account.profile_description', [account.username])" @click="previewHeader">
</button> </button>
<div p4 mt--18 flex flex-col gap-4> <div p4 mt--18 flex flex-col gap-4>
<div relative> <div relative>
@ -122,7 +127,7 @@ watchEffect(() => {
</div> </div>
<div v-if="iconFields.length" flex="~ wrap gap-4"> <div v-if="iconFields.length" flex="~ wrap gap-4">
<div v-for="field in iconFields" :key="field.name" flex="~ gap-1" items-center> <div v-for="field in iconFields" :key="field.name" flex="~ gap-1" items-center>
<div text-secondary :class="getFieldNameIcon(field.name)" :title="field.name" /> <div text-secondary :class="getFieldNameIcon(field.name)" :title="getFieldIconTitle(field.name)" />
<ContentRich text-sm filter-saturate-0 :content="field.value" :emojis="account.emojis" /> <ContentRich text-sm filter-saturate-0 :content="field.value" :emojis="account.emojis" />
</div> </div>
</div> </div>

View File

@ -1,31 +1,46 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account } from 'masto' import type { Account } from 'masto'
defineProps<{ const props = defineProps<{
account: Account account: Account
}>() }>()
const { formatHumanReadableNumber, formatNumber, forSR } = useHumanReadableNumber()
const statusesCount = $computed(() => formatNumber(props.account.statusesCount))
const followingCount = $computed(() => formatHumanReadableNumber(props.account.followingCount))
const followingCountSR = $computed(() => forSR(props.account.followingCount))
const followersCount = $computed(() => formatHumanReadableNumber(props.account.followersCount))
const followersCountSR = $computed(() => forSR(props.account.followersCount))
</script> </script>
<template> <template>
<div flex gap-5> <div flex gap-5>
<NuxtLink :to="getAccountRoute(account)" text-secondary exact-active-class="text-primary"> <NuxtLink :to="getAccountRoute(account)" text-secondary exact-active-class="text-primary">
<template #default="{ isExactActive }"> <template #default="{ isExactActive }">
<i18n-t keypath="account.posts_count"> <i18n-t keypath="account.posts_count" :plural="account.statusesCount">
<span font-bold :class="isExactActive ? 'text-primary' : 'text-base'">{{ formattedNumber(account.statusesCount) }}</span> <span font-bold :class="isExactActive ? 'text-primary' : 'text-base'">{{ statusesCount }}</span>
</i18n-t> </i18n-t>
</template> </template>
</NuxtLink> </NuxtLink>
<NuxtLink :to="getAccountFollowingRoute(account)" text-secondary exact-active-class="text-primary"> <NuxtLink :to="getAccountFollowingRoute(account)" text-secondary exact-active-class="text-primary">
<template #default="{ isExactActive }"> <template #default="{ isExactActive }">
<i18n-t keypath="account.following_count"> <i18n-t keypath="account.following_count">
<span font-bold :class="isExactActive ? 'text-primary' : 'text-base'">{{ humanReadableNumber(account.followingCount) }}</span> <span v-if="followingCountSR">
<span aria-hidden="true" font-bold :class="isExactActive ? 'text-primary' : 'text-base'">{{ followingCount }}</span>
<span sr-only font-bold>{{ account.followingCount }}</span>
</span>
<span v-else font-bold :class="isExactActive ? 'text-primary' : 'text-base'">{{ followingCount }}</span>
</i18n-t> </i18n-t>
</template> </template>
</NuxtLink> </NuxtLink>
<NuxtLink :to="getAccountFollowersRoute(account)" text-secondary exact-active-class="text-primary"> <NuxtLink :to="getAccountFollowersRoute(account)" text-secondary exact-active-class="text-primary">
<template #default="{ isExactActive }"> <template #default="{ isExactActive }">
<i18n-t keypath="account.followers_count"> <i18n-t keypath="account.followers_count" :plural="account.followersCount">
<span font-bold :class="isExactActive ? 'text-primary' : 'text-base'">{{ humanReadableNumber(account.followersCount) }}</span> <span v-if="followersCountSR">
<span aria-hidden="true" font-bold :class="isExactActive ? 'text-primary' : 'text-base'">{{ followersCount }}</span>
<span sr-only font-bold>{{ account.followersCount }}</span>
</span>
<span v-else font-bold :class="isExactActive ? 'text-primary' : 'text-base'">{{ followersCount }}</span>
</i18n-t> </i18n-t>
</template> </template>
</NuxtLink> </NuxtLink>

View File

@ -1,21 +1,24 @@
<script setup lang="ts"> <script setup lang="ts">
const { t } = useI18n()
const buildTime = import.meta.env.__BUILD_TIME__ as string const buildTime = import.meta.env.__BUILD_TIME__ as string
const buildTimeAgo = useTimeAgo(buildTime) const buildTimeDate = new Date(buildTime)
const timeAgoOptions = useTimeAgoOptions()
const buildTimeAgo = useTimeAgo(buildTime, timeAgoOptions)
</script> </script>
<template> <template>
<footer p4 text-sm text-secondary-light flex="~ col"> <footer p4 text-sm text-secondary-light flex="~ col">
<div flex="~ gap2" items-center mb4> <div flex="~ gap2" items-center mb4>
<CommonTooltip :content="t('nav_footer.toggle_theme')"> <CommonTooltip :content="$t('nav_footer.toggle_theme')">
<button flex i-ri:sun-line dark:i-ri:moon-line text-lg :aria-label="t('nav_footer.toggle_theme')" @click="toggleDark()" /> <button flex i-ri:sun-line dark:i-ri:moon-line text-lg :aria-label="$t('nav_footer.toggle_theme')" @click="toggleDark()" />
</CommonTooltip> </CommonTooltip>
<CommonTooltip :content="t('nav_footer.zen_mode')"> <CommonTooltip :content="$t('nav_footer.zen_mode')">
<button <button
flex flex
text-lg text-lg
:class="isZenMode ? 'i-ri:layout-right-2-line' : 'i-ri:layout-right-line'" :class="isZenMode ? 'i-ri:layout-right-2-line' : 'i-ri:layout-right-line'"
:aria-label="t('nav_footer.zen_mode')" :aria-label="$t('nav_footer.zen_mode')"
@click="toggleZenMode()" @click="toggleZenMode()"
/> />
</CommonTooltip> </CommonTooltip>
@ -30,7 +33,7 @@ const buildTimeAgo = useTimeAgo(buildTime)
<div>{{ $t('app_desc_short') }}</div> <div>{{ $t('app_desc_short') }}</div>
<div> <div>
<i18n-t keypath="nav_footer.built_at"> <i18n-t keypath="nav_footer.built_at">
<time :datetime="buildTime" :title="buildTime">{{ buildTimeAgo }}</time> <time :datetime="buildTime" :title="$d(buildTimeDate, 'long')">{{ buildTimeAgo }}</time>
</i18n-t> · <a href="https://github.com/elk-zone/elk" target="_blank">GitHub</a> </i18n-t> · <a href="https://github.com/elk-zone/elk" target="_blank">GitHub</a>
</div> </div>
</footer> </footer>

View File

@ -5,15 +5,30 @@ const { items } = defineProps<{
items: GroupedNotifications items: GroupedNotifications
}>() }>()
const count = computed(() => items.items.length) const { formatHumanReadableNumber, forSR } = useHumanReadableNumber()
const count = $computed(() => items.items.length)
const addSR = $computed(() => forSR(count))
const isExpanded = ref(false) const isExpanded = ref(false)
</script> </script>
<template> <template>
<article flex flex-col> <article flex flex-col>
<div flex ml-4 items-center> <div flex ml-4 items-center>
<div i-ri:user-follow-fill mr-3 color-primary /> <div i-ri:user-follow-fill mr-3 color-primary aria-hidden="true" />
{{ $t('notification.followed_you_count', [`${count}`]) }} <template v-if="addSR">
<span
aria-hidden="true"
>
{{ $t('notification.followed_you_count', count, { named: { followers: formatHumanReadableNumber(count) } }) }}
</span>
<span sr-only>
{{ $t('notification.followed_you_count', count, { named: { followers: count } }) }}
</span>
</template>
<span v-else>
{{ $t('notification.followed_you_count', count, { named: { followers: count } }) }}
</span>
</div> </div>
<div v-if="isExpanded"> <div v-if="isExpanded">
<AccountCard <AccountCard

View File

@ -41,6 +41,7 @@ function go(evt: MouseEvent | KeyboardEvent) {
} }
const createdAt = useFormattedDateTime(status.createdAt) const createdAt = useFormattedDateTime(status.createdAt)
const timeAgoOptions = useTimeAgoOptions()
const timeago = useTimeAgo(() => status.createdAt, timeAgoOptions) const timeago = useTimeAgo(() => status.createdAt, timeAgoOptions)
</script> </script>

View File

@ -10,7 +10,8 @@ function toPercentage(num: number) {
const percentage = 100 * num const percentage = 100 * num
return `${percentage.toFixed(1).replace(/\.?0+$/, '')}%` return `${percentage.toFixed(1).replace(/\.?0+$/, '')}%`
} }
const expiredTimeAgo = useTimeAgo(poll.expiresAt!) const timeAgoOptions = useTimeAgoOptions()
const expiredTimeAgo = useTimeAgo(poll.expiresAt!, timeAgoOptions)
const masto = useMasto() const masto = useMasto()
async function vote(e: Event) { async function vote(e: Event) {

View File

@ -10,6 +10,7 @@ const { data: statusEdits } = useAsyncData(`status:history:${status.id}`, () =>
const showHistory = (edit: StatusEdit) => { const showHistory = (edit: StatusEdit) => {
openEditHistoryDialog(edit) openEditHistoryDialog(edit)
} }
const timeAgoOptions = useTimeAgoOptions()
</script> </script>
<template> <template>
@ -22,7 +23,7 @@ const showHistory = (edit: StatusEdit) => {
> >
{{ getDisplayName(edit.account) }} {{ getDisplayName(edit.account) }}
<i18n-t :keypath="`status_history.${idx === statusEdits.length - 1 ? 'created' : 'edited'}`"> <i18n-t :keypath="`status_history.${idx === statusEdits.length - 1 ? 'created' : 'edited'}`">
{{ useTimeAgo(edit.createdAt, { showSecond: true }).value }} {{ useTimeAgo(edit.createdAt, timeAgoOptions).value }}
</i18n-t> </i18n-t>
</CommonDropdownItem> </CommonDropdownItem>
</template> </template>

View File

@ -15,7 +15,7 @@ const virtualScroller = $(computedEager(() => useFeatureFlags().experimentalVirt
<CommonPaginator v-bind="{ paginator, stream }" :virtual-scroller="virtualScroller"> <CommonPaginator v-bind="{ paginator, stream }" :virtual-scroller="virtualScroller">
<template #updater="{ number, update }"> <template #updater="{ number, update }">
<button py-4 border="b base" flex="~ col" p-3 w-full text-primary font-bold @click="update"> <button py-4 border="b base" flex="~ col" p-3 w-full text-primary font-bold @click="update">
{{ $t('timeline.show_new_items', [number]) }} {{ $t('timeline.show_new_items', number) }}
</button> </button>
</template> </template>
<template #default="{ item, active }"> <template #default="{ item, active }">

View File

@ -1,15 +1,44 @@
import type { MaybeRef } from '@vueuse/shared'
const formatter = Intl.NumberFormat() const formatter = Intl.NumberFormat()
export const humanReadableNumber = (num: number) => { export const humanReadableNumber = (
num: number,
{ k, m }: { k: string; m: string } = { k: 'K', m: 'M' },
useFormatter: Intl.NumberFormat = formatter,
) => {
if (num < 10000) if (num < 10000)
return formatter.format(num) return useFormatter.format(num)
if (num < 1000000) if (num < 1000000)
return `${Math.floor(num / 1000)}K` return `${Math.floor(num / 1000)}${k}`
return `${Math.floor(num / 1000000)}M` return `${Math.floor(num / 1000000)}${m}`
} }
export const formattedNumber = (num: number) => { export const formattedNumber = (num: number, useFormatter: Intl.NumberFormat = formatter) => {
return formatter.format(num) return useFormatter.format(num)
}
export const useHumanReadableNumber = () => {
const i18n = useI18n()
const numberFormatter = $computed(() => Intl.NumberFormat(i18n.locale.value))
return {
formatHumanReadableNumber: (num: MaybeRef<number>) => {
return humanReadableNumber(
unref(num),
{ k: i18n.t('common.kiloSuffix'), m: i18n.t('common.megaSuffix') },
numberFormatter,
)
},
formatNumber: (num: MaybeRef<number>) => {
return formattedNumber(
unref(num),
numberFormatter,
)
},
forSR: (num: MaybeRef<number>) => {
return unref(num) > 10000
},
}
} }

View File

@ -12,34 +12,28 @@ export const useFormattedDateTime = (
}) })
} }
export const timeAgoOptions: UseTimeAgoOptions<false> = { export const useTimeAgoOptions = (): UseTimeAgoOptions<false> => {
const { d, t } = useI18n()
return {
showSecond: true, showSecond: true,
updateInterval: 1_000,
messages: { messages: {
justNow: 'just now', justNow: t('time_ago_options.just_now'),
// just return the value
past: n => n, past: n => n,
future: n => n.match(/\d/) ? `in ${n}` : n, // just return the value
month: (n, past) => n === 1 future: n => n,
? past second: (n, p) => t(`time_ago_options.${p ? 'past' : 'future'}_second`, n),
? 'last month' minute: (n, p) => t(`time_ago_options.${p ? 'past' : 'future'}_minute`, n),
: 'next month' hour: (n, p) => t(`time_ago_options.${p ? 'past' : 'future'}_hour`, n),
: `${n}m`, day: (n, p) => t(`time_ago_options.${p ? 'past' : 'future'}_day`, n),
year: (n, past) => n === 1 week: (n, p) => t(`time_ago_options.${p ? 'past' : 'future'}_week`, n),
? past month: (n, p) => t(`time_ago_options.${p ? 'past' : 'future'}_month`, n),
? 'last year' year: (n, p) => t(`time_ago_options.${p ? 'past' : 'future'}_year`, n),
: 'next year' },
: `${n}y`, fullDateFormatter(date) {
day: (n, past) => n === 1 return d(date, 'long')
? past
? 'yesterday'
: 'tomorrow'
: `${n}d`,
week: (n, past) => n === 1
? past
? 'last week'
: 'next week'
: `${n} week${n > 1 ? 's' : ''}`,
hour: n => `${n}h`,
minute: n => `${n}min`,
second: n => `${n}s`,
}, },
} }
}

View File

@ -14,7 +14,11 @@
"mutuals": "Mutuals", "mutuals": "Mutuals",
"pinned": "Pinned", "pinned": "Pinned",
"posts_count": "{0} Posts", "posts_count": "{0} Posts",
"unfollow": "Unfollow" "unfollow": "Unfollow",
"joined": "Joined",
"profile_description": "{0}'s profile header",
"avatar_description": "{0}'s avatar",
"profile_unavailable": "Profile unavailable"
}, },
"action": { "action": {
"bookmark": "Bookmark", "bookmark": "Bookmark",
@ -41,7 +45,9 @@
"end_of_list": "End of the list", "end_of_list": "End of the list",
"error": "ERROR", "error": "ERROR",
"not_found": "404 Not Found", "not_found": "404 Not Found",
"offline_desc": "Seems like you are offline. Please check your network connection." "offline_desc": "Seems like you are offline. Please check your network connection.",
"kiloSuffix": "K",
"megaSuffix": "M"
}, },
"conversation": { "conversation": {
"with": "with" "with": "with"
@ -97,7 +103,7 @@
"notification": { "notification": {
"favourited_post": "favourited your post", "favourited_post": "favourited your post",
"followed_you": "followed you", "followed_you": "followed you",
"followed_you_count": "{0} people followed you", "followed_you_count": "{n} people followed you",
"missing_type": "MISSING notification.type:", "missing_type": "MISSING notification.type:",
"reblogged_post": "reblogged your post", "reblogged_post": "reblogged your post",
"request_to_follow": "requested to follow you", "request_to_follow": "requested to follow you",
@ -135,20 +141,24 @@
"posts_with_replies": "Posts & Replies" "posts_with_replies": "Posts & Replies"
}, },
"time_ago_options": { "time_ago_options": {
"in": "in",
"just_now": "just now", "just_now": "just now",
"last_month": "last month", "past_second": "just now|{n} second ago|{n} seconds ago",
"last_week": "last week", "future_second": "just now|in {n} second|in {n} seconds",
"last_year": "last year", "past_minute": "0 minutes ago|1 minute ago|{n} minutes ago",
"next_month": "next month", "future_minute": "in 0 minutes|in 1 minute|in {n} minutes",
"next_week": "next week", "past_hour": "0 hours ago|1 hour ago|{n} hours ago",
"next_year": "next year", "future_hour": "in 0 hours|in 1 hour|in {n} hours",
"tomorrow": "tomorrow", "past_day": "0 days ago|yesterday|{n} days ago",
"week": "week", "future_day": "in 0 days|tomorrow|in {n} days",
"yesterday": "yesterday" "past_week": "0 weeks ago|last week|{n} weeks ago",
"future_week": "in 0 weeks|next week|in {n} weeks",
"past_month": "0 months ago|last month|{n} months ago",
"future_month": "in 0 months|next month|in {n} months",
"past_year": "0 years ago|last year|{n} years ago",
"future_year": "in 0 years|next year|in {n} years"
}, },
"timeline": { "timeline": {
"show_new_items": "Show {0} new items" "show_new_items": "Show {n} new items"
}, },
"title": { "title": {
"federated_timeline": "Federated Timeline", "federated_timeline": "Federated Timeline",

View File

@ -6,15 +6,19 @@
"follow": "Seguir", "follow": "Seguir",
"follow_back": "Seguir de vuelta", "follow_back": "Seguir de vuelta",
"follow_requested": "Enviado", "follow_requested": "Enviado",
"followers_count": "{0} Seguidores", "followers_count": "{0} Seguidores|{0} Seguidor|{0} Seguidores",
"following": "Siguiendo", "following": "Siguiendo",
"following_count": "{0} Siguiendo", "following_count": "{0} Siguiendo",
"follows_you": "Te sigue", "follows_you": "Te sigue",
"muted_users": "Usuarios silenciados", "muted_users": "Usuarios silenciados",
"mutuals": "Mutuales", "mutuals": "Mutuales",
"pinned": "Fijado", "pinned": "Fijado",
"posts_count": "{0} publicaciones", "posts_count": "{0} publicaciones|{0} publicación|{0} publicaciones",
"unfollow": "Dejar de seguir" "unfollow": "Dejar de seguir",
"joined": "Se unió",
"profile_description": "encabezado del perfil de {0}",
"avatar_description": "avatar de {0}",
"profile_unavailable": "Perfil no disponible"
}, },
"action": { "action": {
"bookmark": "Añadir marcador", "bookmark": "Añadir marcador",
@ -89,7 +93,7 @@
"notification": { "notification": {
"favourited_post": "marcó tu publicación como favorito", "favourited_post": "marcó tu publicación como favorito",
"followed_you": "te ha seguido", "followed_you": "te ha seguido",
"followed_you_count": "{0} personas te siguieron", "followed_you_count": "{followers} personas te siguieron|{followers} persona te siguió|{followers} personas te siguieron",
"missing_type": "MISSING notification.type:", "missing_type": "MISSING notification.type:",
"reblogged_post": "retooteó tu publicación", "reblogged_post": "retooteó tu publicación",
"request_to_follow": "ha solicitado seguirte", "request_to_follow": "ha solicitado seguirte",
@ -121,18 +125,26 @@
"posts_with_replies": "Publicaciones y respuestas" "posts_with_replies": "Publicaciones y respuestas"
}, },
"time_ago_options": { "time_ago_options": {
"last_month": "mes pasado", "just_now": "ahora mismo",
"last_week": "semana pasada", "past_second": "hace 0 segundos|hace {n} segundo|hace {n} segundos",
"last_year": "año pasado", "future_second": "dentro de 0 segundos|dentro de {n} segundo|dentro de {n} segundos",
"next_month": "próximo mes", "past_minute": "hace 0 minutos|hace 1 minuto|hace {n} minutos",
"next_week": "próxima semana", "future_minute": "dentro de 0 minutos|dentro de 1 minuto|dentro de {n} minutos",
"next_year": "próximo año", "past_hour": "hace 0 horas|hace 1 hora|hace {n} horas",
"future_hour": "dentro de 0 horas|dentro de 1 hora|dentro {n} horas",
"past_day": "hace 0 días|ayer|hace {n} días",
"future_day": "dentro de 0 días|mañana|dentro de {n} días",
"past_week": "hace 0 semanas|la semana pasada|hace {n} semanas",
"future_week": "dentro de 0 semanas|la próxima semana|dentro de {n} semanas",
"past_month": "hace 0 meses|el mes pasado|hace {n} meses",
"future_month": "dentro de 0 meses|el próximo mes|dentro de {n} meses",
"past_year": "hace 0 años|el año pasado|hace {n} años",
"future_year": "dentro de 0 años|el próximo año|dentro de {n} años",
"tomorrow": "mañana", "tomorrow": "mañana",
"week": "semana",
"yesterday": "ayer" "yesterday": "ayer"
}, },
"timeline": { "timeline": {
"show_new_items": "Mostrar {0} nuevas publicaciones" "show_new_items": "Mostrar {n} nuevas publicaciones|Mostrar {n} nueva publicación|Mostrar {n} nuevas publicaciones"
}, },
"title": { "title": {
"federated_timeline": "Línea de tiempo federada", "federated_timeline": "Línea de tiempo federada",

View File

@ -78,7 +78,7 @@
"posts_with_replies": "投稿と返信" "posts_with_replies": "投稿と返信"
}, },
"timeline": { "timeline": {
"show_new_items": "{0}件の新しい投稿" "show_new_items": "{n}件の新しい投稿"
}, },
"title": { "title": {
"federated_timeline": "連合タイムライン", "federated_timeline": "連合タイムライン",

View File

@ -97,7 +97,7 @@
"notification": { "notification": {
"favourited_post": "点赞了你的帖文", "favourited_post": "点赞了你的帖文",
"followed_you": "关注了你", "followed_you": "关注了你",
"followed_you_count": "{0} 人关注了你", "followed_you_count": "{n} 人关注了你",
"missing_type": "未知的通知类型:", "missing_type": "未知的通知类型:",
"reblogged_post": "转发了你的帖文", "reblogged_post": "转发了你的帖文",
"request_to_follow": "请求关注你", "request_to_follow": "请求关注你",
@ -148,7 +148,7 @@
"yesterday": "昨天" "yesterday": "昨天"
}, },
"timeline": { "timeline": {
"show_new_items": "展示 {0} 条新帖文" "show_new_items": "展示 {n} 条新帖文"
}, },
"title": { "title": {
"federated_timeline": "跨站时间线", "federated_timeline": "跨站时间线",

View File

@ -1,6 +1,5 @@
import Inspect from 'vite-plugin-inspect' import Inspect from 'vite-plugin-inspect'
import { isCI, isDevelopment } from 'std-env' import { isCI, isDevelopment } from 'std-env'
export default defineNuxtConfig({ export default defineNuxtConfig({
ssr: false, ssr: false,
modules: [ modules: [
@ -116,6 +115,64 @@ export default defineNuxtConfig({
defaultLocale: 'en-US', defaultLocale: 'en-US',
vueI18n: { vueI18n: {
fallbackLocale: 'en-US', fallbackLocale: 'en-US',
datetimeFormats: {
'en-US': {
long: {
dateStyle: 'long',
timeStyle: 'medium',
/*
year: 'numeric',
month: 'short',
day: 'numeric',
weekday: 'short',
hour: 'numeric',
minute: 'numeric',
*/
},
},
'es-ES': {
long: {
dateStyle: 'long',
timeStyle: 'medium',
/*
year: 'numeric',
month: 'short',
day: 'numeric',
weekday: 'short',
hour: 'numeric',
minute: 'numeric',
hour12: false,
*/
},
},
'ja-JP': {
long: {
dateStyle: 'long',
timeStyle: 'medium',
/*
year: 'numeric',
month: 'short',
day: 'numeric',
weekday: 'short',
hour: 'numeric',
minute: 'numeric',
hour12: true,
*/
},
},
'zh-CN': {
long: {
dateStyle: 'long',
timeStyle: 'medium',
},
},
'de-DE': {
long: {
dateStyle: 'long',
timeStyle: 'medium',
},
},
},
}, },
lazy: true, lazy: true,
}, },

View File

@ -44,7 +44,7 @@ const paginator = $computed(() => tabs.find(t => t.name === tab)!.paginator)
<template> <template>
<div v-if="(account!.discoverable === false)" h-30 flex="~ center" text-secondary-light> <div v-if="(account!.discoverable === false)" h-30 flex="~ center" text-secondary-light>
Profile unavailable {{ $t('account.profile_unavailable') }}
</div> </div>
<div v-else> <div v-else>
<CommonTabs v-model="tab" :options="tabs" command /> <CommonTabs v-model="tab" :options="tabs" command />