Impove Intl formatting (#5372)

* Improve Intl formatting

* Additional fixes

* Fixed formatters were not updated on locale change

* Fixed formatNumber was not updated on locale change

* Additional formatting and fixes after merge

* Run prepr:frontend

* Remove `'` in icon map

* Run `pnpm install`

* fix: lint + import

* Additional fixes

---------

Co-authored-by: Calum H. <calum@modrinth.com>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
Jerozgen
2026-03-10 00:29:32 +03:00
committed by GitHub
parent 9b2f0c88cd
commit f62c60a681
88 changed files with 839 additions and 621 deletions

View File

@@ -1,7 +1,6 @@
<script setup> <script setup>
import { DownloadIcon, HeartIcon, TagIcon } from '@modrinth/assets' import { DownloadIcon, HeartIcon, TagIcon } from '@modrinth/assets'
import { Avatar, FormattedTag, TagItem } from '@modrinth/ui' import { Avatar, FormattedTag, TagItem, useCompactNumber } from '@modrinth/ui'
import { formatNumber } from '@modrinth/utils'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime' import relativeTime from 'dayjs/plugin/relativeTime'
import { computed } from 'vue' import { computed } from 'vue'
@@ -11,6 +10,8 @@ dayjs.extend(relativeTime)
const router = useRouter() const router = useRouter()
const { formatCompactNumber } = useCompactNumber()
const props = defineProps({ const props = defineProps({
project: { project: {
type: Object, type: Object,
@@ -96,13 +97,13 @@ const toTransparent = computed(() => {
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border" class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
> >
<DownloadIcon /> <DownloadIcon />
{{ formatNumber(project.downloads) }} {{ formatCompactNumber(project.downloads) }}
</div> </div>
<div <div
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border" class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
> >
<HeartIcon /> <HeartIcon />
{{ formatNumber(project.follows) }} {{ formatCompactNumber(project.follows) }}
</div> </div>
<div class="flex items-center gap-1 pr-2"> <div class="flex items-center gap-1 pr-2">
<TagIcon /> <TagIcon />

View File

@@ -14,7 +14,7 @@
<div v-if="diffs.length" class="flex flex-col gap-2"> <div v-if="diffs.length" class="flex flex-col gap-2">
<span v-if="publishedDate" class="text-contrast font-semibold">{{ <span v-if="publishedDate" class="text-contrast font-semibold">{{
formatMessage(messages.publishedDate, { date: publishedDate }) formatDate(publishedDate)
}}</span> }}</span>
<div class="flex gap-2"> <div class="flex gap-2">
<div v-if="removedCount" class="flex gap-1 items-center"> <div v-if="removedCount" class="flex gap-1 items-center">
@@ -136,6 +136,7 @@ import {
commonMessages, commonMessages,
defineMessages, defineMessages,
NewModal, NewModal,
useFormatDateTime,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener' import { openUrl } from '@tauri-apps/plugin-opener'
@@ -186,6 +187,7 @@ type ProjectInfo = {
} }
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatDate = useFormatDateTime({ dateStyle: 'long' })
const installStore = useInstall() const installStore = useInstall()
type UpdateCompleteCallback = () => void | Promise<void> type UpdateCompleteCallback = () => void | Promise<void>
@@ -409,10 +411,6 @@ const messages = defineMessages({
defaultMessage: defaultMessage:
'An update is required to play {name}. Please update to the latest version to launch the game.', 'An update is required to play {name}. Please update to the latest version to launch the game.',
}, },
publishedDate: {
id: 'app.modal.update-to-play.published-date',
defaultMessage: '{date, date, long}',
},
removedCount: { removedCount: {
id: 'app.modal.update-to-play.removed-count', id: 'app.modal.update-to-play.removed-count',
defaultMessage: '{count} removed', defaultMessage: '{count} removed',

View File

@@ -14,13 +14,13 @@ import {
injectNotificationManager, injectNotificationManager,
OverflowMenu, OverflowMenu,
SmartClickable, SmartClickable,
useFormatDateTime,
useRelativeTime, useRelativeTime,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { capitalizeString } from '@modrinth/utils' import { capitalizeString } from '@modrinth/utils'
import { convertFileSrc } from '@tauri-apps/api/core' import { convertFileSrc } from '@tauri-apps/api/core'
import type { Dayjs } from 'dayjs' import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs'
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue' import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -36,6 +36,10 @@ import { handleSevereError } from '@/store/error'
const { handleError } = injectNotificationManager() const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const router = useRouter() const router = useRouter()
@@ -145,11 +149,7 @@ onUnmounted(() => {
</div> </div>
<div class="flex items-center gap-2 text-sm text-secondary"> <div class="flex items-center gap-2 text-sm text-secondary">
<div <div
v-tooltip=" v-tooltip="instance.last_played ? formatDateTime(instance.last_played) : null"
instance.last_played
? dayjs(instance.last_played).format('MMMM D, YYYY [at] h:mm A')
: null
"
class="w-fit shrink-0" class="w-fit shrink-0"
:class="{ 'cursor-help smart-clickable:allow-pointer-events': last_played }" :class="{ 'cursor-help smart-clickable:allow-pointer-events': last_played }"
> >

View File

@@ -26,10 +26,12 @@ import {
OverflowMenu, OverflowMenu,
SmartClickable, SmartClickable,
TagItem, TagItem,
useFormatDateTime,
useFormatNumber,
useRelativeTime, useRelativeTime,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { formatNumber, getPingLevel } from '@modrinth/utils' import { getPingLevel } from '@modrinth/utils'
import { convertFileSrc } from '@tauri-apps/api/core' import { convertFileSrc } from '@tauri-apps/api/core'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Tooltip } from 'floating-vue' import { Tooltip } from 'floating-vue'
@@ -51,6 +53,11 @@ import { LockIcon } from '../../../../../../packages/assets/generated-icons'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const formatNumber = useFormatNumber()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const router = useRouter() const router = useRouter()
@@ -258,7 +265,7 @@ const messages = defineMessages({
/> />
<Tooltip :disabled="!hasPlayersTooltip"> <Tooltip :disabled="!hasPlayersTooltip">
<span :class="{ 'cursor-help': hasPlayersTooltip }"> <span :class="{ 'cursor-help': hasPlayersTooltip }">
{{ formatNumber(serverStatus.players?.online, false) }} {{ formatNumber(serverStatus.players?.online) }}
online online
</span> </span>
<template #popper> <template #popper>
@@ -279,9 +286,7 @@ const messages = defineMessages({
</div> </div>
<div class="flex items-center gap-2 text-sm text-secondary"> <div class="flex items-center gap-2 text-sm text-secondary">
<div <div
v-tooltip=" v-tooltip="world.last_played ? formatDateTime(world.last_played) : null"
world.last_played ? dayjs(world.last_played).format('MMMM D, YYYY [at] h:mm A') : null
"
class="w-fit shrink-0" class="w-fit shrink-0"
:class="{ :class="{
'cursor-help smart-clickable:allow-pointer-events': world.last_played, 'cursor-help smart-clickable:allow-pointer-events': world.last_played,

View File

@@ -47,9 +47,6 @@
"app.modal.update-to-play.header": { "app.modal.update-to-play.header": {
"message": "Update to play" "message": "Update to play"
}, },
"app.modal.update-to-play.published-date": {
"message": "{date, date, long}"
},
"app.modal.update-to-play.removed-count": { "app.modal.update-to-play.removed-count": {
"message": "{count} removed" "message": "{count} removed"
}, },

View File

@@ -10,13 +10,7 @@
</div> </div>
<span class="gallery-time"> <span class="gallery-time">
<CalendarIcon /> <CalendarIcon />
{{ {{ formatDate(new Date(image.created)) }}
new Date(image.created).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}}
</span> </span>
</Card> </Card>
</div> </div>
@@ -91,7 +85,7 @@ import {
RightArrowIcon, RightArrowIcon,
XIcon, XIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Button, Card } from '@modrinth/ui' import { Button, Card, useFormatDateTime } from '@modrinth/ui'
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js' import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
@@ -99,6 +93,12 @@ import { trackEvent } from '@/helpers/analytics'
const MC_SERVER_BANNER_NAME = '__mc_server_banner__' const MC_SERVER_BANNER_NAME = '__mc_server_banner__'
const formatDate = useFormatDateTime({
year: 'numeric',
month: 'long',
day: 'numeric',
})
const props = defineProps({ const props = defineProps({
project: { project: {
type: Object, type: Object,

View File

@@ -136,22 +136,7 @@
<div class="metadata-item"> <div class="metadata-item">
<span class="metadata-label">Publication Date</span> <span class="metadata-label">Publication Date</span>
<span class="metadata-value"> <span class="metadata-value">
{{ {{ formatDateTime(version.date_published) }}
new Date(version.date_published).toLocaleString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
})
}}
at
{{
new Date(version.date_published).toLocaleString('en-US', {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: true,
})
}}
</span> </span>
</div> </div>
<div v-if="author" class="metadata-item"> <div v-if="author" class="metadata-item">
@@ -183,7 +168,7 @@
<script setup> <script setup>
import { CheckIcon, DownloadIcon, ExternalIcon, FileIcon, ReportIcon } from '@modrinth/assets' import { CheckIcon, DownloadIcon, ExternalIcon, FileIcon, ReportIcon } from '@modrinth/assets'
import { Avatar, Badge, Breadcrumbs, Button, Card, CopyCode } from '@modrinth/ui' import { Avatar, Badge, Breadcrumbs, Button, Card, CopyCode, useFormatDateTime } from '@modrinth/ui'
import { formatBytes, renderString } from '@modrinth/utils' import { formatBytes, renderString } from '@modrinth/utils'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
@@ -193,6 +178,11 @@ import { get_project_many, get_version_many } from '@/helpers/cache.js'
import { releaseColor } from '@/helpers/utils' import { releaseColor } from '@/helpers/utils'
import { useBreadcrumbs } from '@/store/breadcrumbs' import { useBreadcrumbs } from '@/store/breadcrumbs'
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()
const route = useRoute() const route = useRoute()

View File

@@ -178,12 +178,7 @@
class="categories" class="categories"
/> />
{{ $formatVersion(notif.extra_data.version.game_versions) }} {{ $formatVersion(notif.extra_data.version.game_versions) }}
<span <span v-tooltip="formatDateTime(notif.extra_data.version.date_published)" class="date">
v-tooltip="
$dayjs(notif.extra_data.version.date_published).format('MMMM D, YYYY [at] h:mm A')
"
class="date"
>
{{ formatRelativeTime(notif.extra_data.version.date_published) }} {{ formatRelativeTime(notif.extra_data.version.date_published) }}
</span> </span>
</span> </span>
@@ -197,10 +192,7 @@
<span v-if="notification.read" class="read-badge inline-flex"> <span v-if="notification.read" class="read-badge inline-flex">
<CheckCircleIcon /> Read <CheckCircleIcon /> Read
</span> </span>
<span <span v-tooltip="formatDateTime(notification.created)" class="inline-flex">
v-tooltip="$dayjs(notification.created).format('MMMM D, YYYY [at] h:mm A')"
class="inline-flex"
>
<CalendarIcon class="mr-1" /> Received <CalendarIcon class="mr-1" /> Received
{{ formatRelativeTime(notification.created) }} {{ formatRelativeTime(notification.created) }}
</span> </span>
@@ -338,6 +330,7 @@ import {
DoubleIcon, DoubleIcon,
injectNotificationManager, injectNotificationManager,
ProjectStatusBadge, ProjectStatusBadge,
useFormatDateTime,
useRelativeTime, useRelativeTime,
} from '@modrinth/ui' } from '@modrinth/ui'
import { getUserLink, renderString } from '@modrinth/utils' import { getUserLink, renderString } from '@modrinth/utils'
@@ -351,6 +344,10 @@ import ThreadSummary from './thread/ThreadSummary.vue'
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
const emit = defineEmits(['update:notifications']) const emit = defineEmits(['update:notifications'])
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const props = defineProps({ const props = defineProps({
notification: { notification: {

View File

@@ -1,7 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { FileTextIcon } from '@modrinth/assets' import { FileTextIcon } from '@modrinth/assets'
import { ButtonStyled, defineMessages, PagewideBanner, useVIntl } from '@modrinth/ui' import {
import { formatMoney } from '@modrinth/utils' ButtonStyled,
defineMessages,
PagewideBanner,
useFormatMoney,
useVIntl,
} from '@modrinth/ui'
import { computed } from 'vue' import { computed } from 'vue'
import { getTaxThreshold } from '@/providers/creator-withdraw.ts' import { getTaxThreshold } from '@/providers/creator-withdraw.ts'
@@ -9,6 +14,7 @@ import CreatorTaxFormModal from '~/components/ui/dashboard/CreatorTaxFormModal.v
import { useGeneratedState } from '~/composables/generated' import { useGeneratedState } from '~/composables/generated'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatMoney = useFormatMoney()
const generatedState = useGeneratedState() const generatedState = useGeneratedState()
const taxThreshold = computed(() => getTaxThreshold(generatedState.value?.taxComplianceThresholds)) const taxThreshold = computed(() => getTaxThreshold(generatedState.value?.taxComplianceThresholds))

View File

@@ -1,6 +1,5 @@
<script setup> <script setup>
import { formatMoney, formatNumber } from '@modrinth/utils' import { useFormatDateTime, useFormatMoney, useFormatNumber } from '@modrinth/ui'
import dayjs from 'dayjs'
import VueApexCharts from 'vue3-apexcharts' import VueApexCharts from 'vue3-apexcharts'
const props = defineProps({ const props = defineProps({
@@ -18,7 +17,6 @@ const props = defineProps({
}, },
formatLabels: { formatLabels: {
type: Function, type: Function,
default: (label) => dayjs(label).format('MMM D'),
}, },
colors: { colors: {
type: Array, type: Array,
@@ -78,8 +76,15 @@ const props = defineProps({
}, },
}) })
const formatNumber = useFormatNumber()
const formatMoney = useFormatMoney()
const formatDate = useFormatDateTime({
month: 'short',
day: 'numeric',
})
function formatTooltipValue(value, props) { function formatTooltipValue(value, props) {
return props.isMoney ? formatMoney(value, false) : formatNumber(value, false) return props.isMoney ? formatMoney(value) : formatNumber(value)
} }
function generateListEntry(value, index, _, w, props) { function generateListEntry(value, index, _, w, props) {
@@ -99,7 +104,7 @@ function generateListEntry(value, index, _, w, props) {
function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) { function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
const label = w.globals.lastXAxis.categories?.[dataPointIndex] const label = w.globals.lastXAxis.categories?.[dataPointIndex]
const formattedLabel = props.formatLabels(label) const formattedLabel = props.formatLabels ? props.formatLabels(label) : formatDate(label)
let tooltip = `<div class="bar-tooltip"> let tooltip = `<div class="bar-tooltip">
<div class="seperated-entry title"> <div class="seperated-entry title">

View File

@@ -21,7 +21,7 @@
ref="tinyDownloadChart" ref="tinyDownloadChart"
:title="`Downloads`" :title="`Downloads`"
color="var(--color-brand)" color="var(--color-brand)"
:value="formatNumber(analytics.formattedData.value.downloads.sum, false)" :value="formatNumber(analytics.formattedData.value.downloads.sum)"
:data="analytics.formattedData.value.downloads.chart.sumData" :data="analytics.formattedData.value.downloads.chart.sumData"
:labels="analytics.formattedData.value.downloads.chart.labels" :labels="analytics.formattedData.value.downloads.chart.labels"
suffix="<svg xmlns='http://www.w3.org/2000/svg' class='h-6 w-6' fill='none' viewBox='0 0 24 24' stroke='currentColor' stroke-width='2'><path stroke-linecap='round' stroke-linejoin='round' d='M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4' /></svg>" suffix="<svg xmlns='http://www.w3.org/2000/svg' class='h-6 w-6' fill='none' viewBox='0 0 24 24' stroke='currentColor' stroke-width='2'><path stroke-linecap='round' stroke-linejoin='round' d='M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4' /></svg>"
@@ -40,7 +40,7 @@
ref="tinyViewChart" ref="tinyViewChart"
:title="`Views`" :title="`Views`"
color="var(--color-blue)" color="var(--color-blue)"
:value="formatNumber(analytics.formattedData.value.views.sum, false)" :value="formatNumber(analytics.formattedData.value.views.sum)"
:data="analytics.formattedData.value.views.chart.sumData" :data="analytics.formattedData.value.views.chart.sumData"
:labels="analytics.formattedData.value.views.chart.labels" :labels="analytics.formattedData.value.views.chart.labels"
suffix="<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z'/><circle cx='12' cy='12' r='3'/></svg>" suffix="<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z'/><circle cx='12' cy='12' r='3'/></svg>"
@@ -57,7 +57,7 @@
ref="tinyRevenueChart" ref="tinyRevenueChart"
:title="`Revenue`" :title="`Revenue`"
color="var(--color-purple)" color="var(--color-purple)"
:value="formatMoney(analytics.formattedData.value.revenue.sum, false)" :value="formatMoney(analytics.formattedData.value.revenue.sum)"
:data="analytics.formattedData.value.revenue.chart.sumData" :data="analytics.formattedData.value.revenue.chart.sumData"
:labels="analytics.formattedData.value.revenue.chart.labels" :labels="analytics.formattedData.value.revenue.chart.labels"
is-money is-money
@@ -221,7 +221,7 @@
><template v-if="name.toLowerCase() === 'xx' || !name">Other</template> ><template v-if="name.toLowerCase() === 'xx' || !name">Other</template>
<template v-else>{{ countryCodeToName(name) }}</template> <template v-else>{{ countryCodeToName(name) }}</template>
</strong> </strong>
<span class="data-point">{{ formatNumber(count) }}</span> <span class="data-point">{{ formatCompactNumber(count) }}</span>
</div> </div>
<div <div
v-tooltip=" v-tooltip="
@@ -280,7 +280,7 @@
<template v-if="name.toLowerCase() === 'xx' || !name">Other</template> <template v-if="name.toLowerCase() === 'xx' || !name">Other</template>
<template v-else>{{ countryCodeToName(name) }}</template> <template v-else>{{ countryCodeToName(name) }}</template>
</strong> </strong>
<span class="data-point">{{ formatNumber(count) }}</span> <span class="data-point">{{ formatCompactNumber(count) }}</span>
</div> </div>
<div <div
v-tooltip=" v-tooltip="
@@ -310,8 +310,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { DownloadIcon, PaletteIcon, UpdatedIcon } from '@modrinth/assets' import { DownloadIcon, PaletteIcon, UpdatedIcon } from '@modrinth/assets'
import { Button, Card, DropdownSelect } from '@modrinth/ui' import {
import { capitalizeString, formatMoney, formatNumber } from '@modrinth/utils' Button,
Card,
DropdownSelect,
useCompactNumber,
useFormatMoney,
useFormatNumber,
} from '@modrinth/ui'
import { capitalizeString } from '@modrinth/utils'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { computed } from 'vue' import { computed } from 'vue'
@@ -325,6 +332,10 @@ import {
intToRgba, intToRgba,
} from '~/utils/analytics.js' } from '~/utils/analytics.js'
const formatNumber = useFormatNumber()
const { formatCompactNumber } = useCompactNumber()
const formatMoney = useFormatMoney()
const router = useNativeRouter() const router = useNativeRouter()
const theme = useTheme() const theme = useTheme()

View File

@@ -55,9 +55,9 @@ import {
commonMessages, commonMessages,
formFieldPlaceholders, formFieldPlaceholders,
StyledInput, StyledInput,
useFormatMoney,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
const props = withDefaults( const props = withDefaults(
@@ -82,6 +82,7 @@ const emit = defineEmits<{
}>() }>()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatMoney = useFormatMoney()
const amountInput = ref<InstanceType<typeof StyledInput> | null>(null) const amountInput = ref<InstanceType<typeof StyledInput> | null>(null)
const safeMaxAmount = computed(() => { const safeMaxAmount = computed(() => {

View File

@@ -35,7 +35,7 @@
>{{ formatTransactionStatus(transaction.status) }} <BulletDivider >{{ formatTransactionStatus(transaction.status) }} <BulletDivider
/></span> /></span>
</template> </template>
{{ dayjs(transaction.created).format('MMM DD YYYY') }} {{ formatDate(transaction.created) }}
<template v-if="transaction.type === 'withdrawal' && transaction.fee"> <template v-if="transaction.type === 'withdrawal' && transaction.fee">
<BulletDivider /> Fee {{ formatMoney(transaction.fee) }} <BulletDivider /> Fee {{ formatMoney(transaction.fee) }}
</template> </template>
@@ -79,10 +79,11 @@ import {
ButtonStyled, ButtonStyled,
getCurrencyIcon, getCurrencyIcon,
injectNotificationManager, injectNotificationManager,
useFormatDateTime,
useFormatMoney,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { capitalizeString, formatMoney } from '@modrinth/utils' import { capitalizeString } from '@modrinth/utils'
import dayjs from 'dayjs'
import { Tooltip } from 'floating-vue' import { Tooltip } from 'floating-vue'
import { useGeneratedState } from '~/composables/generated' import { useGeneratedState } from '~/composables/generated'
@@ -188,6 +189,8 @@ function formatTransactionStatus(status: string): string {
} }
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatMoney = useFormatMoney()
const formatDate = useFormatDateTime({ dateStyle: 'medium' })
function formatMethodName(method: string | undefined, method_id: string | undefined): string { function formatMethodName(method: string | undefined, method_id: string | undefined): string {
if (!method) return 'Unknown' if (!method) return 'Unknown'

View File

@@ -57,8 +57,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { LoaderCircleIcon } from '@modrinth/assets' import { LoaderCircleIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '@modrinth/ui' import { defineMessages, useFormatMoney, useVIntl } from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { computed } from 'vue' import { computed } from 'vue'
const props = withDefaults( const props = withDefaults(
@@ -78,6 +77,7 @@ const props = withDefaults(
) )
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatMoney = useFormatMoney()
const amountInUsd = computed(() => { const amountInUsd = computed(() => {
if (props.isGiftCard && shouldShowExchangeRate.value) { if (props.isGiftCard && shouldShowExchangeRate.value) {
@@ -119,31 +119,13 @@ const formattedLocalCurrency = computed(() => {
if (!shouldShowExchangeRate.value || !netAmountInLocalCurrency.value || !props.localCurrency) if (!shouldShowExchangeRate.value || !netAmountInLocalCurrency.value || !props.localCurrency)
return '' return ''
try { return formatMoney(netAmountInLocalCurrency.value, props.localCurrency)
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: props.localCurrency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(netAmountInLocalCurrency.value)
} catch {
return `${props.localCurrency} ${netAmountInLocalCurrency.value.toFixed(2)}`
}
}) })
const formattedLocalCurrencyAmount = computed(() => { const formattedLocalCurrencyAmount = computed(() => {
if (!shouldShowExchangeRate.value || !localCurrencyAmount.value || !props.localCurrency) return '' if (!shouldShowExchangeRate.value || !localCurrencyAmount.value || !props.localCurrency) return ''
try { return formatMoney(localCurrencyAmount.value, props.localCurrency)
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: props.localCurrency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(localCurrencyAmount.value)
} catch {
return `${props.localCurrency} ${localCurrencyAmount.value.toFixed(2)}`
}
}) })
const messages = defineMessages({ const messages = defineMessages({

View File

@@ -124,9 +124,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineMessages, IntlFormatted, normalizeChildren, useVIntl } from '@modrinth/ui' import {
import { formatMoney } from '@modrinth/utils' defineMessages,
import dayjs from 'dayjs' IntlFormatted,
normalizeChildren,
useFormatDateTime,
useFormatMoney,
useVIntl,
} from '@modrinth/ui'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import ConfettiExplosion from 'vue-confetti-explosion' import ConfettiExplosion from 'vue-confetti-explosion'
@@ -135,6 +140,8 @@ import { getRailConfig } from '@/utils/muralpay-rails'
const { withdrawData } = useWithdrawContext() const { withdrawData } = useWithdrawContext()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatMoney = useFormatMoney()
const formatDate = useFormatDateTime({ dateStyle: 'long' })
const result = computed(() => withdrawData.value.result) const result = computed(() => withdrawData.value.result)
@@ -149,7 +156,7 @@ onMounted(() => {
const formattedDate = computed(() => { const formattedDate = computed(() => {
if (!result.value?.created) return 'N/A' if (!result.value?.created) return 'N/A'
return dayjs(result.value.created).format('MMMM D, YYYY') return formatDate(result.value.created)
}) })
const selectedRail = computed(() => { const selectedRail = computed(() => {
@@ -185,16 +192,7 @@ const formattedLocalCurrency = computed(() => {
if (!shouldShowExchangeRate.value || !netAmountInLocalCurrency.value || !localCurrency.value) if (!shouldShowExchangeRate.value || !netAmountInLocalCurrency.value || !localCurrency.value)
return '' return ''
try { return formatMoney(netAmountInLocalCurrency.value, localCurrency.value)
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: localCurrency.value,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(netAmountInLocalCurrency.value)
} catch {
return `${localCurrency.value} ${netAmountInLocalCurrency.value.toFixed(2)}`
}
}) })
const isMuralPayWithdrawal = computed(() => { const isMuralPayWithdrawal = computed(() => {

View File

@@ -88,9 +88,9 @@ import {
IntlFormatted, IntlFormatted,
normalizeChildren, normalizeChildren,
useDebugLogger, useDebugLogger,
useFormatMoney,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { useGeolocation } from '@vueuse/core' import { useGeolocation } from '@vueuse/core'
import { useCountries, useFormattedCountries, useUserCountry } from '@/composables/country.ts' import { useCountries, useFormattedCountries, useUserCountry } from '@/composables/country.ts'
@@ -115,6 +115,7 @@ const userCountry = useUserCountry()
const allCountries = useCountries() const allCountries = useCountries()
const { coords } = useGeolocation() const { coords } = useGeolocation()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatMoney = useFormatMoney()
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
const auth = await useAuth() const auth = await useAuth()

View File

@@ -80,9 +80,9 @@ import {
defineMessages, defineMessages,
IntlFormatted, IntlFormatted,
normalizeChildren, normalizeChildren,
useFormatMoney,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { computed } from 'vue' import { computed } from 'vue'
import { getTaxThreshold, getTaxThresholdActual } from '@/providers/creator-withdraw.ts' import { getTaxThreshold, getTaxThresholdActual } from '@/providers/creator-withdraw.ts'
@@ -94,6 +94,7 @@ const props = defineProps<{
}>() }>()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatMoney = useFormatMoney()
const generatedState = useGeneratedState() const generatedState = useGeneratedState()
const taxThreshold = computed(() => getTaxThreshold(generatedState.value.taxComplianceThresholds)) const taxThreshold = computed(() => getTaxThreshold(generatedState.value.taxComplianceThresholds))

View File

@@ -350,9 +350,9 @@ import {
paymentMethodMessages, paymentMethodMessages,
StyledInput, StyledInput,
useDebugLogger, useDebugLogger,
useFormatMoney,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { useDebounceFn } from '@vueuse/core' import { useDebounceFn } from '@vueuse/core'
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
@@ -365,6 +365,7 @@ const debug = useDebugLogger('TremendousDetailsStage')
const { withdrawData, maxWithdrawAmount, availableMethods, paymentOptions, calculateFees } = const { withdrawData, maxWithdrawAmount, availableMethods, paymentOptions, calculateFees } =
useWithdrawContext() useWithdrawContext()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatMoney = useFormatMoney()
const auth = await useAuth() const auth = await useAuth()
const userEmail = computed(() => { const userEmail = computed(() => {
@@ -587,16 +588,7 @@ function formatAmountForDisplay(
if (!currencyCode || currencyCode === 'USD' || !rate) { if (!currencyCode || currencyCode === 'USD' || !rate) {
return formatMoney(localAmount) return formatMoney(localAmount)
} }
try { return formatMoney(localAmount, currencyCode)
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currencyCode,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(localAmount)
} catch {
return `${currencyCode} ${localAmount.toFixed(2)}`
}
} }
const useFixedDenominations = computed(() => { const useFixedDenominations = computed(() => {

View File

@@ -73,7 +73,7 @@
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span <span
v-tooltip="`Since ${queuedDate.toLocaleString()}`" v-tooltip="`Since ${formatDateTimeFull(queuedDate.toDate())}`"
class="text-base text-secondary" class="text-base text-secondary"
:class="{ :class="{
'text-red': daysInQueue > 4, 'text-red': daysInQueue > 4,
@@ -120,6 +120,7 @@ import {
injectNotificationManager, injectNotificationManager,
OverflowMenu, OverflowMenu,
type OverflowMenuOption, type OverflowMenuOption,
useFormatDateTime,
useRelativeTime, useRelativeTime,
} from '@modrinth/ui' } from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils' import { formatProjectType } from '@modrinth/utils'
@@ -130,6 +131,17 @@ import type { ModerationProject } from '~/helpers/moderation'
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const formatDateTimeFull = useFormatDateTime({
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
timeZoneName: 'short',
timeZone: 'UTC',
})
const baseId = useId() const baseId = useId()

View File

@@ -32,7 +32,7 @@
<div class="flex flex-row items-center gap-2 self-end sm:self-auto"> <div class="flex flex-row items-center gap-2 self-end sm:self-auto">
<span <span
v-tooltip="formatExactDate(report.created)" v-tooltip="formatDateTime(report.created)"
class="cursor-help whitespace-nowrap text-sm text-secondary" class="cursor-help whitespace-nowrap text-sm text-secondary"
> >
{{ formatRelativeTime(report.created) }} {{ formatRelativeTime(report.created) }}
@@ -80,7 +80,7 @@
<span <span
v-if="report.user?.created" v-if="report.user?.created"
v-tooltip="formatExactDate(report.user.created)" v-tooltip="formatDateTime(report.user.created)"
class="cursor-help text-sm text-secondary" class="cursor-help text-sm text-secondary"
> >
Joined {{ formatRelativeTime(report.user.created) }} Joined {{ formatRelativeTime(report.user.created) }}
@@ -190,7 +190,7 @@ import {
LinkIcon, LinkIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { type ExtendedReport, reportQuickReplies } from '@modrinth/moderation' import { type ExtendedReport, reportQuickReplies } from '@modrinth/moderation'
import type { OverflowMenuOption } from '@modrinth/ui' import { type OverflowMenuOption, useFormatDateTime } from '@modrinth/ui'
import { import {
Avatar, Avatar,
ButtonStyled, ButtonStyled,
@@ -201,7 +201,6 @@ import {
useRelativeTime, useRelativeTime,
} from '@modrinth/ui' } from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils' import { formatProjectType } from '@modrinth/utils'
import dayjs from 'dayjs'
import { computed } from 'vue' import { computed } from 'vue'
import { isStaff } from '~/helpers/users.js' import { isStaff } from '~/helpers/users.js'
@@ -305,10 +304,10 @@ async function reopenReport() {
} }
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
function formatExactDate(date: string): string { timeStyle: 'short',
return dayjs(date).format('MMMM D, YYYY [at] h:mm A') dateStyle: 'long',
} })
function updateThread(newThread: any) { function updateThread(newThread: any) {
if (props.report.thread) { if (props.report.thread) {

View File

@@ -28,6 +28,7 @@ import {
injectNotificationManager, injectNotificationManager,
OverflowMenu, OverflowMenu,
type OverflowMenuOption, type OverflowMenuOption,
useFormatDateTime,
} from '@modrinth/ui' } from '@modrinth/ui'
import { import {
capitalizeString, capitalizeString,
@@ -46,6 +47,16 @@ import ThreadView from '~/components/ui/thread/ThreadView.vue'
const auth = await useAuth() const auth = await useAuth()
const featureFlags = useFeatureFlags() const featureFlags = useFeatureFlags()
const formatDateTimeUtc = useFormatDateTime({
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'short',
timeZone: 'UTC',
})
type FlattenedFileReport = Labrinth.TechReview.Internal.FileReport & { type FlattenedFileReport = Labrinth.TechReview.Internal.FileReport & {
id: string id: string
version_id: string version_id: string
@@ -763,7 +774,7 @@ const reviewSummaryPreview = computed(() => {
const totalDecisions = totalSafe + totalUnsafe const totalDecisions = totalSafe + totalUnsafe
if (totalDecisions === 0) return '' if (totalDecisions === 0) return ''
const timestamp = dayjs().utc().format('MMMM D, YYYY [at] h:mm A [UTC]') const timestamp = formatDateTimeUtc(dayjs().toDate())
let markdown = `## Tech Review Summary\n*${timestamp}*\n\n` let markdown = `## Tech Review Summary\n*${timestamp}*\n\n`
markdown += `<details>\n<summary>File Details (${totalSafe} safe, ${totalUnsafe} unsafe)</summary>\n\n` markdown += `<details>\n<summary>File Details (${totalSafe} safe, ${totalUnsafe} unsafe)</summary>\n\n`

View File

@@ -94,7 +94,7 @@
<span>{{ report.reporterUser.username }}</span> <span>{{ report.reporterUser.username }}</span>
</nuxt-link> </nuxt-link>
<span>&nbsp;</span> <span>&nbsp;</span>
<span v-tooltip="$dayjs(report.created).format('MMMM D, YYYY [at] h:mm A')">{{ <span v-tooltip="formatDateTime(report.created)">{{
formatRelativeTime(report.created) formatRelativeTime(report.created)
}}</span> }}</span>
<CopyCode v-if="flags.developerMode" :text="report.id" class="report-id" /> <CopyCode v-if="flags.developerMode" :text="report.id" class="report-id" />
@@ -104,13 +104,17 @@
<script setup> <script setup>
import { ReportIcon, UnknownIcon, VersionIcon } from '@modrinth/assets' import { ReportIcon, UnknownIcon, VersionIcon } from '@modrinth/assets'
import { Avatar, Badge, CopyCode, useRelativeTime } from '@modrinth/ui' import { Avatar, Badge, CopyCode, useFormatDateTime, useRelativeTime } from '@modrinth/ui'
import { formatProjectType, renderHighlightedString } from '@modrinth/utils' import { formatProjectType, renderHighlightedString } from '@modrinth/utils'
import ThreadSummary from '~/components/ui/thread/ThreadSummary.vue' import ThreadSummary from '~/components/ui/thread/ThreadSummary.vue'
import { getProjectTypeForUrl } from '~/helpers/projects.js' import { getProjectTypeForUrl } from '~/helpers/projects.js'
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
defineProps({ defineProps({
report: { report: {

View File

@@ -79,6 +79,7 @@ import {
getFileExtensionIcon, getFileExtensionIcon,
isEditableFile as isEditableFileExt, isEditableFile as isEditableFileExt,
isImageFile, isImageFile,
useFormatDateTime,
} from '@modrinth/ui' } from '@modrinth/ui'
import { computed, h, ref, shallowRef } from 'vue' import { computed, h, ref, shallowRef } from 'vue'
import { renderToString } from 'vue/server-renderer' import { renderToString } from 'vue/server-renderer'
@@ -121,6 +122,14 @@ const units = Object.freeze(['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'])
const route = shallowRef(useRoute()) const route = shallowRef(useRoute())
const router = useRouter() const router = useRouter()
const formatDateTime = useFormatDateTime({
year: '2-digit',
month: '2-digit',
day: '2-digit',
hour: 'numeric',
minute: 'numeric',
})
const containerClasses = computed(() => [ const containerClasses = computed(() => [
'group m-0 flex w-full select-none items-center justify-between overflow-hidden border-0 border-t border-solid border-surface-3 px-4 py-3 focus:!outline-none', 'group m-0 flex w-full select-none items-center justify-between overflow-hidden border-0 border-t border-solid border-surface-3 px-4 py-3 focus:!outline-none',
props.index % 2 === 0 ? 'bg-surface-2' : 'bg-surface-3', props.index % 2 === 0 ? 'bg-surface-2' : 'bg-surface-3',
@@ -177,28 +186,12 @@ const iconComponent = computed(() => {
const formattedModifiedDate = computed(() => { const formattedModifiedDate = computed(() => {
const date = new Date(props.modified * 1000) const date = new Date(props.modified * 1000)
return `${date.toLocaleDateString('en-US', { return formatDateTime(date)
month: '2-digit',
day: '2-digit',
year: '2-digit',
})}, ${date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: 'numeric',
hour12: true,
})}`
}) })
const formattedCreationDate = computed(() => { const formattedCreationDate = computed(() => {
const date = new Date(props.created * 1000) const date = new Date(props.created * 1000)
return `${date.toLocaleDateString('en-US', { return formatDateTime(date)
month: '2-digit',
day: '2-digit',
year: '2-digit',
})}, ${date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: 'numeric',
hour12: true,
})}`
}) })
const isEditableFile = computed(() => { const isEditableFile = computed(() => {

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { MessageDescriptor } from '@modrinth/ui' import { type MessageDescriptor, useFormatPrice } from '@modrinth/ui'
import { ButtonStyled, defineMessage, defineMessages, ServersSpecs, useVIntl } from '@modrinth/ui' import { ButtonStyled, defineMessage, defineMessages, ServersSpecs, useVIntl } from '@modrinth/ui'
import { formatPrice } from '@modrinth/utils'
const { formatMessage, locale } = useVIntl() const { formatMessage } = useVIntl()
const formatPrice = useFormatPrice()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'select' | 'scroll-to-faq'): void (e: 'select' | 'scroll-to-faq'): void
@@ -132,7 +132,7 @@ const billingMonths = computed(() => {
</div> </div>
</div> </div>
<span class="m-0 text-2xl font-bold text-contrast"> <span class="m-0 text-2xl font-bold text-contrast">
{{ formatPrice(locale, price / billingMonths, currency, true) }} {{ formatPrice(price / billingMonths, currency, true) }}
<span class="text-lg font-semibold text-secondary"> <span class="text-lg font-semibold text-secondary">
/ month<template v-if="interval !== 'monthly'">, billed {{ interval }}</template> / month<template v-if="interval !== 'monthly'">, billed {{ interval }}</template>
</span> </span>

View File

@@ -6,14 +6,22 @@ import {
NOTICE_LEVELS, NOTICE_LEVELS,
ServerNotice, ServerNotice,
TagItem, TagItem,
useFormatDateTime,
useRelativeTime, useRelativeTime,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import type { ServerNotice as ServerNoticeType } from '@modrinth/utils' import type { ServerNotice as ServerNoticeType } from '@modrinth/utils'
import dayjs from 'dayjs'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const formatDateTimeShortMonth = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'medium',
})
defineProps<{ defineProps<{
notice: ServerNoticeType notice: ServerNoticeType
@@ -27,17 +35,14 @@ defineProps<{
</div> </div>
<div class="text-sm"> <div class="text-sm">
<span v-if="notice.announce_at"> <span v-if="notice.announce_at">
{{ dayjs(notice.announce_at).format('MMM D, YYYY [at] h:mm A') }} ({{ {{ formatDateTimeShortMonth(notice.announce_at) }} ({{
formatRelativeTime(notice.announce_at) formatRelativeTime(notice.announce_at)
}}) }})
</span> </span>
<template v-else> Never begins </template> <template v-else> Never begins </template>
</div> </div>
<div class="text-sm"> <div class="text-sm">
<span <span v-if="notice.expires" v-tooltip="formatDateTime(notice.expires)">
v-if="notice.expires"
v-tooltip="dayjs(notice.expires).format('MMMM D, YYYY [at] h:mm A')"
>
{{ formatRelativeTime(notice.expires) }} {{ formatRelativeTime(notice.expires) }}
</span> </span>
<template v-else> Never expires </template> <template v-else> Never expires </template>

View File

@@ -99,7 +99,7 @@
</span> </span>
</div> </div>
<span class="message__date"> <span class="message__date">
<span v-tooltip="$dayjs(message.created).format('MMMM D, YYYY [at] h:mm A')"> <span v-tooltip="formatDateTime(message.created)">
{{ timeSincePosted }} {{ timeSincePosted }}
</span> </span>
</span> </span>
@@ -131,7 +131,14 @@ import {
ScaleIcon, ScaleIcon,
TrashIcon, TrashIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { AutoLink, Avatar, Badge, OverflowMenu, useRelativeTime } from '@modrinth/ui' import {
AutoLink,
Avatar,
Badge,
OverflowMenu,
useFormatDateTime,
useRelativeTime,
} from '@modrinth/ui'
import { renderString } from '@modrinth/utils' import { renderString } from '@modrinth/utils'
import { isStaff } from '~/helpers/users.js' import { isStaff } from '~/helpers/users.js'
@@ -186,6 +193,11 @@ const formattedMessage = computed(() => {
}) })
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const timeSincePosted = ref(formatRelativeTime(props.message.created)) const timeSincePosted = ref(formatRelativeTime(props.message.created))
const isPrivateMessage = computed(() => { const isPrivateMessage = computed(() => {

View File

@@ -1,26 +0,0 @@
const formatters = new WeakMap<object, Intl.NumberFormat>()
export function useCompactNumber(truncate = false, fractionDigits = 2, locale?: string) {
const context = {}
let formatter = formatters.get(context)
if (!formatter) {
formatter = new Intl.NumberFormat(locale, {
notation: 'compact',
maximumFractionDigits: fractionDigits,
})
formatters.set(context, formatter)
}
function format(value: number): string {
let formattedValue = value
if (truncate) {
const scale = Math.pow(10, fractionDigits)
formattedValue = Math.floor(value * scale) / scale
}
return formatter!.format(formattedValue)
}
return format
}

View File

@@ -393,7 +393,7 @@
"message": "No projects in collection yet" "message": "No projects in collection yet"
}, },
"collection.label.projects-count": { "collection.label.projects-count": {
"message": "{count, plural, =0 {No projects yet} one {<stat>{count}</stat> project} other {<stat>{count}</stat> {type}}}" "message": "{count, plural, =0 {No projects yet} other {<stat>{count}</stat> {type}}}"
}, },
"collection.label.updated-at": { "collection.label.updated-at": {
"message": "Updated {ago}" "message": "Updated {ago}"
@@ -591,7 +591,7 @@
"message": "Try adjusting your filters or search terms." "message": "Try adjusting your filters or search terms."
}, },
"dashboard.collections.label.projects-count": { "dashboard.collections.label.projects-count": {
"message": "{count, plural, one {{count} project} other {{count} projects}}" "message": "{count} {countPlural, plural, one {project} other {projects}}"
}, },
"dashboard.collections.label.search-input": { "dashboard.collections.label.search-input": {
"message": "Search your collections" "message": "Search your collections"
@@ -1437,7 +1437,7 @@
"message": "For Players" "message": "For Players"
}, },
"landing.section.for-players.tagline": { "landing.section.for-players.tagline": {
"message": "Discover over {count} creations" "message": "Discover over {count, number} creations"
}, },
"landing.subheading": { "landing.subheading": {
"message": "Discover, play, and share Minecraft content through our open-source platform built for the community." "message": "Discover, play, and share Minecraft content through our open-source platform built for the community."
@@ -2040,7 +2040,7 @@
"message": "Collection" "message": "Collection"
}, },
"profile.label.downloads": { "profile.label.downloads": {
"message": "{count} {count, plural, one {download} other {downloads}}" "message": "{count} {countPlural, plural, one {download} other {downloads}}"
}, },
"profile.label.joined": { "profile.label.joined": {
"message": "Joined" "message": "Joined"
@@ -2061,7 +2061,7 @@
"message": "Organizations" "message": "Organizations"
}, },
"profile.label.projects": { "profile.label.projects": {
"message": "{count} {count, plural, one {project} other {projects}}" "message": "{count} {countPlural, plural, one {project} other {projects}}"
}, },
"profile.label.saving": { "profile.label.saving": {
"message": "Saving..." "message": "Saving..."

View File

@@ -613,7 +613,7 @@
<IntlFormatted <IntlFormatted
:message-id="messages.serversPromoPricing" :message-id="messages.serversPromoPricing"
:values="{ :values="{
price: formatPrice(locale, 500, 'USD', true), price: formatPrice(500, 'USD', true),
}" }"
> >
<template #small="{ children }"> <template #small="{ children }">
@@ -945,7 +945,7 @@
{{ {{
capitalizeString( capitalizeString(
formatMessage(commonMessages.projectFollowers, { formatMessage(commonMessages.projectFollowers, {
count: formatNumber(project.followers, false), count: project.followers,
}), }),
) )
}} }}
@@ -953,7 +953,7 @@
</div> </div>
<div <div
v-if="project.approved" v-if="project.approved"
v-tooltip="$dayjs(project.approved).format('MMMM D, YYYY [at] h:mm A')" v-tooltip="formatDateTime(project.approved)"
class="details-list__item" class="details-list__item"
> >
<CalendarIcon aria-hidden="true" /> <CalendarIcon aria-hidden="true" />
@@ -968,11 +968,7 @@
</div> </div>
</div> </div>
<div <div v-else v-tooltip="formatDateTime(project.published)" class="details-list__item">
v-else
v-tooltip="$dayjs(project.published).format('MMMM D, YYYY [at] h:mm A')"
class="details-list__item"
>
<CalendarIcon aria-hidden="true" /> <CalendarIcon aria-hidden="true" />
<div> <div>
{{ {{
@@ -983,7 +979,7 @@
<div <div
v-if="project.status === 'processing' && project.queued" v-if="project.status === 'processing' && project.queued"
v-tooltip="$dayjs(project.queued).format('MMMM D, YYYY [at] h:mm A')" v-tooltip="formatDateTime(project.queued)"
class="details-list__item" class="details-list__item"
> >
<ScaleIcon aria-hidden="true" /> <ScaleIcon aria-hidden="true" />
@@ -1000,7 +996,7 @@
<div <div
v-if="versions.length > 0 && project.updated" v-if="versions.length > 0 && project.updated"
v-tooltip="$dayjs(project.updated).format('MMMM D, YYYY [at] h:mm A')" v-tooltip="formatDateTime(project.updated)"
class="details-list__item" class="details-list__item"
> >
<VersionIcon aria-hidden="true" /> <VersionIcon aria-hidden="true" />
@@ -1099,17 +1095,13 @@ import {
StyledInput, StyledInput,
TagItem, TagItem,
useDebugLogger, useDebugLogger,
useFormatDateTime,
useFormatPrice,
useRelativeTime, useRelativeTime,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import VersionSummary from '@modrinth/ui/src/components/version/VersionSummary.vue' import VersionSummary from '@modrinth/ui/src/components/version/VersionSummary.vue'
import { import { capitalizeString, formatProjectType, renderString } from '@modrinth/utils'
capitalizeString,
formatNumber,
formatPrice,
formatProjectType,
renderString,
} from '@modrinth/utils'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { useLocalStorage } from '@vueuse/core' import { useLocalStorage } from '@vueuse/core'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@@ -1150,7 +1142,12 @@ const tags = useGeneratedState()
const flags = useFeatureFlags() const flags = useFeatureFlags()
const cosmetics = useCosmetics() const cosmetics = useCosmetics()
const { locale, formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatPrice = useFormatPrice()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const debug = useDebugLogger('DownloadModal') const debug = useDebugLogger('DownloadModal')

View File

@@ -50,7 +50,7 @@
</span> </span>
<span> <span>
on on
{{ $dayjs(version.date_published).format('MMM D, YYYY') }}</span {{ formatDate(version.date_published) }}</span
> >
</div> </div>
<a <a
@@ -86,12 +86,23 @@
</template> </template>
<script setup> <script setup>
import { DownloadIcon, SpinnerIcon } from '@modrinth/assets' import { DownloadIcon, SpinnerIcon } from '@modrinth/assets'
import { injectModrinthClient, injectProjectPageContext, Pagination } from '@modrinth/ui' import {
injectModrinthClient,
injectProjectPageContext,
Pagination,
useFormatDateTime,
} from '@modrinth/ui'
import VersionFilterControl from '@modrinth/ui/src/components/version/VersionFilterControl.vue' import VersionFilterControl from '@modrinth/ui/src/components/version/VersionFilterControl.vue'
import { renderHighlightedString } from '@modrinth/utils' import { renderHighlightedString } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query' import { useQuery } from '@tanstack/vue-query'
import { onMounted } from 'vue' import { onMounted } from 'vue'
const formatDate = useFormatDateTime({
month: 'short',
day: 'numeric',
year: 'numeric',
})
const { projectV2, versions, versionsLoading, loadVersions } = injectProjectPageContext() const { projectV2, versions, versionsLoading, loadVersions } = injectProjectPageContext()
// Load versions on mount (client-side) // Load versions on mount (client-side)

View File

@@ -264,7 +264,7 @@
<div class="gallery-bottom"> <div class="gallery-bottom">
<div class="gallery-created"> <div class="gallery-created">
<CalendarIcon aria-hidden="true" aria-label="Date created" /> <CalendarIcon aria-hidden="true" aria-label="Date created" />
{{ $dayjs(item.created).format('MMMM D, YYYY') }} {{ formatDate(item.created) }}
</div> </div>
<div v-if="currentMember" class="gallery-buttons input-group"> <div v-if="currentMember" class="gallery-buttons input-group">
<button <button
@@ -341,11 +341,18 @@ import {
injectProjectPageContext, injectProjectPageContext,
NewModal as Modal, NewModal as Modal,
StyledInput, StyledInput,
useFormatDateTime,
} from '@modrinth/ui' } from '@modrinth/ui'
import { useEventListener, useLocalStorage } from '@vueuse/core' import { useEventListener, useLocalStorage } from '@vueuse/core'
import { isPermission } from '~/utils/permissions.ts' import { isPermission } from '~/utils/permissions.ts'
const formatDate = useFormatDateTime({
year: 'numeric',
month: 'long',
day: 'numeric',
})
// Router // Router
const router = useRouter() const router = useRouter()

View File

@@ -235,7 +235,7 @@
<div class="gallery-bottom"> <div class="gallery-bottom">
<div class="gallery-created"> <div class="gallery-created">
<CalendarIcon aria-hidden="true" aria-label="Date created" /> <CalendarIcon aria-hidden="true" aria-label="Date created" />
{{ $dayjs(item.created).format('MMMM D, YYYY') }} {{ formatDate(item.created) }}
</div> </div>
<div v-if="currentMember" class="gallery-buttons input-group"> <div v-if="currentMember" class="gallery-buttons input-group">
<button <button
@@ -300,10 +300,17 @@ import {
injectProjectPageContext, injectProjectPageContext,
NewModal as Modal, NewModal as Modal,
StyledInput, StyledInput,
useFormatDateTime,
} from '@modrinth/ui' } from '@modrinth/ui'
import { isPermission } from '~/utils/permissions.ts' import { isPermission } from '~/utils/permissions.ts'
const formatDate = useFormatDateTime({
year: 'numeric',
month: 'long',
day: 'numeric',
})
const { const {
projectV2: project, projectV2: project,
currentMember, currentMember,

View File

@@ -368,7 +368,7 @@
<div v-if="!isEditing"> <div v-if="!isEditing">
<h4>Publication date</h4> <h4>Publication date</h4>
<span> <span>
{{ $dayjs(version.date_published).format('MMMM D, YYYY [at] h:mm A') }} {{ formatDateTime(version.date_published) }}
</span> </span>
</div> </div>
<div v-if="!isEditing && version.author"> <div v-if="!isEditing && version.author">
@@ -437,6 +437,7 @@ import {
injectNotificationManager, injectNotificationManager,
injectProjectPageContext, injectProjectPageContext,
StyledInput, StyledInput,
useFormatDateTime,
} from '@modrinth/ui' } from '@modrinth/ui'
import { formatBytes, renderHighlightedString } from '@modrinth/utils' import { formatBytes, renderHighlightedString } from '@modrinth/utils'
import { Multiselect } from 'vue-multiselect' import { Multiselect } from 'vue-multiselect'
@@ -460,6 +461,11 @@ const auth = await useAuth()
const tags = useGeneratedState() const tags = useGeneratedState()
const flags = useFeatureFlags() const flags = useFeatureFlags()
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const formatDate = useFormatDateTime({ dateStyle: 'medium' })
// Helper for accessing nuxt app $formatVersion // Helper for accessing nuxt app $formatVersion
const formatVersionDisplay = (versions: string[]) => (data as any).$formatVersion(versions) const formatVersionDisplay = (versions: string[]) => (data as any).$formatVersion(versions)
@@ -697,9 +703,9 @@ const description = computed(
version.value.loaders ?? [] version.value.loaders ?? []
) )
.map((x: string) => x.charAt(0).toUpperCase() + x.slice(1)) .map((x: string) => x.charAt(0).toUpperCase() + x.slice(1))
.join(' & ')}. Published on ${data .join(
.$dayjs(version.value.date_published) ' & ',
.format('MMM D, YYYY')}. ${version.value.downloads} downloads.`, )}. Published on ${formatDate(version.value.date_published)}. ${version.value.downloads} downloads.`,
) )
const usesFeaturedVersions = computed(() => const usesFeaturedVersions = computed(() =>

View File

@@ -169,7 +169,7 @@
</span> </span>
<div class="mb-4 mt-2 flex w-full items-center gap-1 text-sm text-secondary"> <div class="mb-4 mt-2 flex w-full items-center gap-1 text-sm text-secondary">
{{ capitalizeString(subscription.interval) }} ⋅ {{ subscription.status }} ⋅ {{ capitalizeString(subscription.interval) }} ⋅ {{ subscription.status }} ⋅
{{ dayjs(subscription.created).format('MMMM D, YYYY [at] h:mma') }} ({{ {{ formatDateTime(subscription.created) }} ({{
formatRelativeTime(subscription.created) formatRelativeTime(subscription.created)
}}) }})
</div> </div>
@@ -239,7 +239,7 @@
</span> </span>
<template v-if="charge.status !== 'cancelled'"> <template v-if="charge.status !== 'cancelled'">
{{ formatPrice(vintl.locale, charge.amount, charge.currency_code) }} {{ formatPrice(charge.amount, charge.currency_code) }}
</template> </template>
</span> </span>
<span class="text-sm text-secondary"> <span class="text-sm text-secondary">
@@ -252,13 +252,13 @@
<span v-else-if="charge.status === 'cancelled'" class="font-bold">Ends:</span> <span v-else-if="charge.status === 'cancelled'" class="font-bold">Ends:</span>
<span v-else-if="charge.type === 'refund'" class="font-bold">Issued:</span> <span v-else-if="charge.type === 'refund'" class="font-bold">Issued:</span>
<span v-else class="font-bold">Due:</span> <span v-else class="font-bold">Due:</span>
{{ dayjs(charge.due).format('MMMM D, YYYY [at] h:mma') }} {{ formatDateTime(charge.due) }}
<span class="text-secondary">({{ formatRelativeTime(charge.due) }}) </span> <span class="text-secondary">({{ formatRelativeTime(charge.due) }}) </span>
</span> </span>
<span v-if="charge.last_attempt != null" class="text-sm text-secondary"> <span v-if="charge.last_attempt != null" class="text-sm text-secondary">
<span v-if="charge.status === 'failed'" class="font-bold">Last attempt:</span> <span v-if="charge.status === 'failed'" class="font-bold">Last attempt:</span>
<span v-else class="font-bold">Charged:</span> <span v-else class="font-bold">Charged:</span>
{{ dayjs(charge.last_attempt).format('MMMM D, YYYY [at] h:mma') }} {{ formatDateTime(charge.last_attempt) }}
<span class="text-secondary" <span class="text-secondary"
>({{ formatRelativeTime(charge.last_attempt) }}) >({{ formatRelativeTime(charge.last_attempt) }})
</span> </span>
@@ -268,9 +268,9 @@
{{ charge.type }} {{ charge.type }}
{{ formatPrice(vintl.locale, charge.amount, charge.currency_code) }} {{ formatPrice(charge.amount, charge.currency_code) }}
{{ dayjs(charge.due).format('YYYY-MM-DD h:mma') }} {{ formatDateTimeShort(charge.due) }}
<template v-if="charge.subscription_interval"> <template v-if="charge.subscription_interval">
⋅ {{ charge.subscription_interval }} ⋅ {{ charge.subscription_interval }}
</template> </template>
@@ -332,16 +332,30 @@ import {
NewModal, NewModal,
StyledInput, StyledInput,
Toggle, Toggle,
useFormatDateTime,
useFormatPrice,
useRelativeTime, useRelativeTime,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { capitalizeString, formatPrice } from '@modrinth/utils' import { capitalizeString } from '@modrinth/utils'
import { DEFAULT_CREDIT_EMAIL_MESSAGE } from '@modrinth/utils/utils.ts' import { DEFAULT_CREDIT_EMAIL_MESSAGE } from '@modrinth/utils/utils.ts'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import ModrinthServersIcon from '~/components/ui/servers/ModrinthServersIcon.vue' import ModrinthServersIcon from '~/components/ui/servers/ModrinthServersIcon.vue'
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
const formatPrice = useFormatPrice()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const formatDateTimeShort = useFormatDateTime({
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: 'numeric',
minute: 'numeric',
})
const route = useRoute() const route = useRoute()
const vintl = useVIntl() const vintl = useVIntl()

View File

@@ -159,16 +159,13 @@
</div> </div>
<div class="text-sm"> <div class="text-sm">
<span v-if="notice.announce_at"> <span v-if="notice.announce_at">
{{ dayjs(notice.announce_at).format('MMM D, YYYY [at] h:mm A') }} {{ formatDateTimeShortMonth(notice.announce_at) }}
({{ formatRelativeTime(notice.announce_at) }}) ({{ formatRelativeTime(notice.announce_at) }})
</span> </span>
<template v-else> Never begins </template> <template v-else> Never begins </template>
</div> </div>
<div class="text-sm"> <div class="text-sm">
<span <span v-if="notice.expires" v-tooltip="formatDateTime(notice.expires)">
v-if="notice.expires"
v-tooltip="dayjs(notice.expires).format('MMMM D, YYYY [at] h:mm A')"
>
{{ formatRelativeTime(notice.expires) }} {{ formatRelativeTime(notice.expires) }}
</span> </span>
<template v-else> Never expires </template> <template v-else> Never expires </template>
@@ -276,6 +273,7 @@ import {
StyledInput, StyledInput,
TagItem, TagItem,
Toggle, Toggle,
useFormatDateTime,
useRelativeTime, useRelativeTime,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
@@ -290,6 +288,14 @@ import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const formatDateTimeShortMonth = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'medium',
})
const notices = ref<ServerNoticeType[]>([]) const notices = ref<ServerNoticeType[]>([])
const createNoticeModal = ref<InstanceType<typeof NewModal>>() const createNoticeModal = ref<InstanceType<typeof NewModal>>()

View File

@@ -76,11 +76,11 @@
</div> </div>
</div> </div>
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-secondary"> <div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-secondary">
<span v-tooltip="dayjs(batch.created_at).format('MMMM D, YYYY [at] h:mm A')"> <span v-tooltip="formatDateTime(batch.created_at)">
Created {{ formatRelativeTime(batch.created_at) }} Created {{ formatRelativeTime(batch.created_at) }}
</span> </span>
<span></span> <span></span>
<span v-tooltip="dayjs(batch.scheduled_at).format('MMMM D, YYYY [at] h:mm A')"> <span v-tooltip="formatDateTime(batch.scheduled_at)">
Scheduled {{ formatRelativeTime(batch.scheduled_at) }} Scheduled {{ formatRelativeTime(batch.scheduled_at) }}
</span> </span>
<template v-if="batch.provision_options?.region"> <template v-if="batch.provision_options?.region">
@@ -116,6 +116,7 @@ import {
injectNotificationManager, injectNotificationManager,
Pagination, Pagination,
TagItem, TagItem,
useFormatDateTime,
useRelativeTime, useRelativeTime,
} from '@modrinth/ui' } from '@modrinth/ui'
import type { User } from '@modrinth/utils' import type { User } from '@modrinth/utils'
@@ -127,6 +128,10 @@ import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
// Types // Types
interface ProvisionOptions { interface ProvisionOptions {

View File

@@ -180,9 +180,11 @@
count: formatCompactNumber(projects?.length || 0), count: formatCompactNumber(projects?.length || 0),
type: formatMessage( type: formatMessage(
commonProjectTypeSentenceMessages[ commonProjectTypeSentenceMessages[
projectTypes.length === 1 ? projectTypes[0] : 'project' projectTypes.length === 1 && projects?.length > 1
? projectTypes[0]
: 'project'
], ],
{ count: projects?.length || 0 }, { count: formatCompactNumberPlural(projects?.length || 0) },
), ),
}" }"
> >
@@ -247,7 +249,7 @@
> >
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<span <span
v-tooltip="dayjs(collection.created).format('MMMM D, YYYY [at] h:mm A')" v-tooltip="formatDateTime(collection.created)"
class="flex w-fit items-center gap-2" class="flex w-fit items-center gap-2"
> >
<CalendarIcon aria-hidden="true" /> <CalendarIcon aria-hidden="true" />
@@ -259,7 +261,7 @@
</span> </span>
<span <span
v-if="showUpdatedDate" v-if="showUpdatedDate"
v-tooltip="dayjs(collection.updated).format('MMMM D, YYYY [at] h:mm A')" v-tooltip="formatDateTime(collection.updated)"
class="flex w-fit items-center gap-2" class="flex w-fit items-center gap-2"
> >
<UpdatedIcon aria-hidden="true" /> <UpdatedIcon aria-hidden="true" />
@@ -410,6 +412,8 @@ import {
RadioButtons, RadioButtons,
SidebarCard, SidebarCard,
StyledInput, StyledInput,
useCompactNumber,
useFormatDateTime,
useRelativeTime, useRelativeTime,
useSavable, useSavable,
useVIntl, useVIntl,
@@ -425,7 +429,11 @@ const { handleError } = injectNotificationManager()
const api = injectModrinthClient() const api = injectModrinthClient()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const formatCompactNumber = useCompactNumber() const { formatCompactNumber, formatCompactNumberPlural } = useCompactNumber()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const route = useNativeRoute() const route = useNativeRoute()
const router = useRouter() const router = useRouter()
@@ -491,8 +499,7 @@ const messages = defineMessages({
}, },
projectsCountLabel: { projectsCountLabel: {
id: 'collection.label.projects-count', id: 'collection.label.projects-count',
defaultMessage: defaultMessage: '{count, plural, =0 {No projects yet} other {<stat>{count}</stat> {type}}}',
'{count, plural, =0 {No projects yet} one {<stat>{count}</stat> project} other {<stat>{count}</stat> {type}}}',
}, },
removeProjectButton: { removeProjectButton: {
id: 'collection.button.remove-project', id: 'collection.button.remove-project',

View File

@@ -63,6 +63,7 @@
{{ {{
formatMessage(messages.projectsCountLabel, { formatMessage(messages.projectsCountLabel, {
count: formatCompactNumber(user ? user.follows.length : 0), count: formatCompactNumber(user ? user.follows.length : 0),
countPlural: formatCompactNumberPlural(user ? user.follows.length : 0),
}) })
}} }}
</div> </div>
@@ -91,6 +92,7 @@
{{ {{
formatMessage(messages.projectsCountLabel, { formatMessage(messages.projectsCountLabel, {
count: formatCompactNumber(collection.projects?.length || 0), count: formatCompactNumber(collection.projects?.length || 0),
countPlural: formatCompactNumberPlural(collection.projects?.length || 0),
}) })
}} }}
</div> </div>
@@ -154,13 +156,14 @@ import {
defineMessages, defineMessages,
DropdownSelect, DropdownSelect,
StyledInput, StyledInput,
useCompactNumber,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue' import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatCompactNumber = useCompactNumber() const { formatCompactNumber, formatCompactNumberPlural } = useCompactNumber()
const messages = defineMessages({ const messages = defineMessages({
createNewButton: { createNewButton: {
@@ -177,7 +180,7 @@ const messages = defineMessages({
}, },
projectsCountLabel: { projectsCountLabel: {
id: 'dashboard.collections.label.projects-count', id: 'dashboard.collections.label.projects-count',
defaultMessage: '{count, plural, one {{count} project} other {{count} projects}}', defaultMessage: '{count} {countPlural, plural, one {project} other {projects}}',
}, },
searchInputLabel: { searchInputLabel: {
id: 'dashboard.collections.label.search-input', id: 'dashboard.collections.label.search-input',

View File

@@ -67,7 +67,7 @@
></span> ></span>
{{ {{
formatMessage(messages.estimatedWithDate, { formatMessage(messages.estimatedWithDate, {
date: date.date ? dayjs(date.date).format('MMM D, YYYY') : '', date: date.date ? formatDate(date.date) : '',
}) })
}} }}
<Tooltip theme="dismissable-prompt" :triggers="['hover', 'focus']" no-auto-focus> <Tooltip theme="dismissable-prompt" :triggers="['hover', 'focus']" no-auto-focus>
@@ -260,8 +260,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ArrowUpRightIcon, InProgressIcon, UnknownIcon } from '@modrinth/assets' import { ArrowUpRightIcon, InProgressIcon, UnknownIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '@modrinth/ui' import { defineMessages, useFormatDateTime, useFormatMoney, useVIntl } from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Tooltip } from 'floating-vue' import { Tooltip } from 'floating-vue'
@@ -271,6 +270,8 @@ import CreatorWithdrawModal from '~/components/ui/dashboard/CreatorWithdrawModal
import RevenueTransaction from '~/components/ui/dashboard/RevenueTransaction.vue' import RevenueTransaction from '~/components/ui/dashboard/RevenueTransaction.vue'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatMoney = useFormatMoney()
const formatDate = useFormatDateTime({ dateStyle: 'medium' })
await useAuth() await useAuth()

View File

@@ -90,8 +90,15 @@ import {
GenericListIcon, GenericListIcon,
SpinnerIcon, SpinnerIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { ButtonStyled, Combobox, defineMessages, useVIntl } from '@modrinth/ui' import {
import { formatMoney } from '@modrinth/utils' ButtonStyled,
Combobox,
defineMessages,
useFormatDateTime,
useFormatMoney,
useVIntl,
} from '@modrinth/ui'
import { capitalizeString } from '@modrinth/utils'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import RevenueTransaction from '~/components/ui/dashboard/RevenueTransaction.vue' import RevenueTransaction from '~/components/ui/dashboard/RevenueTransaction.vue'
@@ -99,6 +106,12 @@ import { useGeneratedState } from '~/composables/generated'
import { findRail } from '~/utils/muralpay-rails' import { findRail } from '~/utils/muralpay-rails'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatMoney = useFormatMoney()
const formatMonth = useFormatDateTime({
year: 'numeric',
month: 'long',
})
const generatedState = useGeneratedState() const generatedState = useGeneratedState()
useHead({ useHead({
@@ -152,7 +165,7 @@ function getPeriodLabel(date) {
} else if (txnDate.isSame(now.subtract(1, 'month'), 'month')) { } else if (txnDate.isSame(now.subtract(1, 'month'), 'month')) {
return 'Last month' return 'Last month'
} else { } else {
return txnDate.format('MMMM YYYY') return capitalizeString(formatMonth(txnDate.toDate()))
} }
} }

View File

@@ -616,7 +616,7 @@
<p v-if="lowestPrice" class="m-0 text-sm"> <p v-if="lowestPrice" class="m-0 text-sm">
{{ {{
formatMessage(messages.startingAtPrice, { formatMessage(messages.startingAtPrice, {
price: formatPrice(locale, lowestPrice, selectedCurrency, true), price: formatPrice(lowestPrice, selectedCurrency, true),
}) })
}} }}
</p> </p>
@@ -644,10 +644,10 @@ import {
injectNotificationManager, injectNotificationManager,
IntlFormatted, IntlFormatted,
ModrinthServersPurchaseModal, ModrinthServersPurchaseModal,
useFormatPrice,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { monthsInInterval } from '@modrinth/ui/src/utils/billing.ts' import { monthsInInterval } from '@modrinth/ui/src/utils/billing.ts'
import { formatPrice } from '@modrinth/utils'
import { computed } from 'vue' import { computed } from 'vue'
import { useBaseFetch } from '@/composables/fetch.js' import { useBaseFetch } from '@/composables/fetch.js'
@@ -678,7 +678,8 @@ if (affiliateCode.value) {
} }
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
const { locale, formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatPrice = useFormatPrice()
const flags = useFeatureFlags() const flags = useFeatureFlags()
const messages = defineMessages({ const messages = defineMessages({

View File

@@ -75,7 +75,7 @@
<div class="section-header"> <div class="section-header">
<div class="section-label green">{{ formatMessage(messages.forPlayersLabel) }}</div> <div class="section-label green">{{ formatMessage(messages.forPlayersLabel) }}</div>
<h2 class="section-tagline"> <h2 class="section-tagline">
{{ formatMessage(messages.discoverCreationsTagline, { count: formattedProjectCount }) }} {{ formatMessage(messages.discoverCreationsTagline, { count: PROJECT_COUNT }) }}
</h2> </h2>
<p class="section-description"> <p class="section-description">
{{ formatMessage(messages.playersDescription) }} {{ formatMessage(messages.playersDescription) }}
@@ -466,8 +466,6 @@ const searchQuery = ref('leave')
const sortType = ref('relevance') const sortType = ref('relevance')
const PROJECT_COUNT = 100000 const PROJECT_COUNT = 100000
const formatNumber = new Intl.NumberFormat().format
const formattedProjectCount = computed(() => formatNumber(PROJECT_COUNT))
const auth = await useAuth() const auth = await useAuth()
@@ -526,7 +524,7 @@ const messages = defineMessages({
}, },
discoverCreationsTagline: { discoverCreationsTagline: {
id: 'landing.section.for-players.tagline', id: 'landing.section.for-players.tagline',
defaultMessage: 'Discover over {count} creations', defaultMessage: 'Discover over {count, number} creations',
}, },
shareContentTagline: { shareContentTagline: {
id: 'landing.section.for-creators.tagline', id: 'landing.section.for-creators.tagline',

View File

@@ -111,7 +111,7 @@
</tr> </tr>
<tr> <tr>
<td>End of the month</td> <td>End of the month</td>
<td>{{ formatDate(endOfMonthDate) }}</td> <td>{{ formatDate(endOfMonthDate.toDate()) }}</td>
</tr> </tr>
<tr> <tr>
<td>NET 60 policy applied</td> <td>NET 60 policy applied</td>
@@ -119,7 +119,7 @@
</tr> </tr>
<tr class="final-result"> <tr class="final-result">
<td>Available for withdrawal</td> <td>Available for withdrawal</td>
<td>{{ formatDate(withdrawalDate) }}</td> <td>{{ formatDate(withdrawalDate.toDate()) }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -162,11 +162,17 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { StyledInput } from '@modrinth/ui' import { StyledInput, useFormatDateTime, useFormatMoney } from '@modrinth/ui'
import { formatDate, formatMoney } from '@modrinth/utils'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
const formatMoney = useFormatMoney()
const formatDate = useFormatDateTime({
month: 'long',
day: 'numeric',
year: 'numeric',
})
const description = const description =
'Information about the Rewards Program of Modrinth, an open source modding platform focused on Minecraft.' 'Information about the Rewards Program of Modrinth, an open source modding platform focused on Minecraft.'

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { GitGraphIcon, RssIcon } from '@modrinth/assets' import { GitGraphIcon, RssIcon } from '@modrinth/assets'
import { articles as rawArticles } from '@modrinth/blog' import { articles as rawArticles } from '@modrinth/blog'
import { Avatar, ButtonStyled } from '@modrinth/ui' import { Avatar, ButtonStyled, useFormatDateTime } from '@modrinth/ui'
import type { User } from '@modrinth/utils' import type { User } from '@modrinth/utils'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { computed, onMounted } from 'vue' import { computed, onMounted } from 'vue'
@@ -12,6 +12,8 @@ import ShareArticleButtons from '~/components/ui/ShareArticleButtons.vue'
const config = useRuntimeConfig() const config = useRuntimeConfig()
const route = useRoute() const route = useRoute()
const formatDate = useFormatDateTime({ dateStyle: 'long' })
const rawArticle = rawArticles.find((article) => article.slug === route.params.slug) const rawArticle = rawArticles.find((article) => article.slug === route.params.slug)
if (!rawArticle) { if (!rawArticle) {
@@ -157,10 +159,10 @@ onMounted(() => {
</nuxt-link> </nuxt-link>
</template> </template>
<span class="hidden md:block"></span> <span class="hidden md:block"></span>
<span class="hidden md:block"> {{ dayjsDate.format('MMMM D, YYYY') }}</span> <span class="hidden md:block"> {{ formatDate(dayjsDate.toDate()) }}</span>
</div> </div>
<span class="text-sm text-secondary sm:text-base md:hidden"> <span class="text-sm text-secondary sm:text-base md:hidden">
Posted on {{ dayjsDate.format('MMMM D, YYYY') }}</span Posted on {{ formatDate(dayjsDate.toDate()) }}</span
> >
<ShareArticleButtons :title="article.title" :url="articleUrl" /> <ShareArticleButtons :title="article.title" :url="articleUrl" />
<img <img

View File

@@ -1,12 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ChevronRightIcon, GitGraphIcon, RssIcon } from '@modrinth/assets' import { ChevronRightIcon, GitGraphIcon, RssIcon } from '@modrinth/assets'
import { articles as rawArticles } from '@modrinth/blog' import { articles as rawArticles } from '@modrinth/blog'
import { ButtonStyled, NewsArticleCard } from '@modrinth/ui' import { ButtonStyled, NewsArticleCard, useFormatDateTime } from '@modrinth/ui'
import dayjs from 'dayjs'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import NewsletterButton from '~/components/ui/NewsletterButton.vue' import NewsletterButton from '~/components/ui/NewsletterButton.vue'
const formatDate = useFormatDateTime({ dateStyle: 'long' })
const articles = ref( const articles = ref(
rawArticles rawArticles
.map((article) => ({ .map((article) => ({
@@ -83,7 +84,7 @@ useSeoMeta({
</h3> </h3>
<p class="m-0 text-lg leading-tight">{{ featuredArticle?.summary }}</p> <p class="m-0 text-lg leading-tight">{{ featuredArticle?.summary }}</p>
<div class="mt-auto text-secondary"> <div class="mt-auto text-secondary">
{{ dayjs(featuredArticle?.date).format('MMMM D, YYYY') }} {{ formatDate(featuredArticle?.date) }}
</div> </div>
</div> </div>
</article> </article>

View File

@@ -19,7 +19,7 @@
</nuxt-link> </nuxt-link>
</h2> </h2>
<span> <span>
{{ formatNumber(acceptedMembers?.length || 0) }} {{ formatCompactNumber(acceptedMembers?.length || 0) }}
member<template v-if="acceptedMembers?.length !== 1">s</template> member<template v-if="acceptedMembers?.length !== 1">s</template>
</span> </span>
</div> </div>
@@ -89,7 +89,7 @@
projects projects
</div> </div>
<div <div
v-tooltip="sumDownloads.toLocaleString()" v-tooltip="formatNumber(sumDownloads)"
class="flex items-center gap-2 font-semibold" class="flex items-center gap-2 font-semibold"
> >
<DownloadIcon class="h-6 w-6 text-secondary" /> <DownloadIcon class="h-6 w-6 text-secondary" />
@@ -293,10 +293,11 @@ import {
OverflowMenu, OverflowMenu,
ProjectCard, ProjectCard,
ProjectCardList, ProjectCardList,
useCompactNumber,
useFormatNumber,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import type { Organization, ProjectStatus, ProjectType } from '@modrinth/utils' import type { Organization, ProjectStatus, ProjectType } from '@modrinth/utils'
import { formatNumber } from '@modrinth/utils'
import UpToDate from '~/assets/images/illustrations/up_to_date.svg?component' import UpToDate from '~/assets/images/illustrations/up_to_date.svg?component'
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue' import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
@@ -318,7 +319,8 @@ type ProjectV3 = Labrinth.Projects.v3.Project & {
const vintl = useVIntl() const vintl = useVIntl()
const { formatMessage } = vintl const { formatMessage } = vintl
const formatCompactNumber = useCompactNumber(true) const formatNumber = useFormatNumber()
const { formatCompactNumber } = useCompactNumber()
const auth: { user: any } & any = await useAuth() const auth: { user: any } & any = await useAuth()
const user = await useUser() const user = await useUser()

View File

@@ -31,7 +31,7 @@
anytime. anytime.
</p> </p>
<p class="m-0 text-[2rem] font-bold text-purple"> <p class="m-0 text-[2rem] font-bold text-purple">
{{ formatPrice(vintl.locale, price.prices.intervals.monthly, price.currency_code) }}/mo {{ formatPrice(price.prices.intervals.monthly, price.currency_code) }}/mo
</p> </p>
<p class="m-0 mb-4 text-secondary"> <p class="m-0 mb-4 text-secondary">
or save or save
@@ -86,14 +86,15 @@
</template> </template>
<script setup> <script setup>
import { HeartIcon, ModrinthPlusIcon, SettingsIcon, SparklesIcon, StarIcon } from '@modrinth/assets' import { HeartIcon, ModrinthPlusIcon, SettingsIcon, SparklesIcon, StarIcon } from '@modrinth/assets'
import { injectNotificationManager, PurchaseModal, useVIntl } from '@modrinth/ui' import { injectNotificationManager, PurchaseModal, useFormatPrice } from '@modrinth/ui'
import { calculateSavings, formatPrice, getCurrency } from '@modrinth/utils' import { calculateSavings, getCurrency } from '@modrinth/utils'
import { useBaseFetch } from '@/composables/fetch.js' import { useBaseFetch } from '@/composables/fetch.js'
import { isPermission } from '@/utils/permissions.ts' import { isPermission } from '@/utils/permissions.ts'
import { products } from '~/generated/state.json' import { products } from '~/generated/state.json'
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
const formatPrice = useFormatPrice()
const title = 'Subscribe to Modrinth Plus!' const title = 'Subscribe to Modrinth Plus!'
const description = const description =
@@ -116,8 +117,6 @@ useHead({
], ],
}) })
const vintl = useVIntl()
const config = useRuntimeConfig() const config = useRuntimeConfig()
const auth = await useAuth() const auth = await useAuth()

View File

@@ -182,7 +182,7 @@
<div> <div>
{{ {{
formatMessage(messages.createdOn, { formatMessage(messages.createdOn, {
date: new Date(app.created).toLocaleDateString(), date: formatDate(new Date(app.created)),
}) })
}} }}
</div> </div>
@@ -257,6 +257,7 @@ import {
IntlFormatted, IntlFormatted,
normalizeChildren, normalizeChildren,
StyledInput, StyledInput,
useFormatDateTime,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
@@ -272,6 +273,7 @@ import {
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatDate = useFormatDateTime()
definePageMeta({ definePageMeta({
middleware: 'auth', middleware: 'auth',

View File

@@ -25,12 +25,12 @@
</template> </template>
</span> </span>
<span>{{ formatPrice(vintl.locale, charge.amount, charge.currency_code) }}</span> <span>{{ formatPrice(charge.amount, charge.currency_code) }}</span>
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Badge :color="charge.status === 'succeeded' ? 'green' : 'red'" :type="charge.status" /> <Badge :color="charge.status === 'succeeded' ? 'green' : 'red'" :type="charge.status" />
{{ $dayjs(charge.due).format('YYYY-MM-DD') }} {{ formatDate(charge.due) }}
</div> </div>
</div> </div>
</div> </div>
@@ -38,8 +38,7 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { Badge, Breadcrumbs, useVIntl } from '@modrinth/ui' import { Badge, Breadcrumbs, useFormatDateTime, useFormatPrice } from '@modrinth/ui'
import { formatPrice } from '@modrinth/utils'
import { products } from '~/generated/state.json' import { products } from '~/generated/state.json'
@@ -47,7 +46,12 @@ definePageMeta({
middleware: 'auth', middleware: 'auth',
}) })
const vintl = useVIntl() const formatPrice = useFormatPrice()
const formatDate = useFormatDateTime({
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
const { data: charges } = await useAsyncData( const { data: charges } = await useAsyncData(
'billing/payments', 'billing/payments',

View File

@@ -51,7 +51,6 @@
<template v-if="midasCharge"> <template v-if="midasCharge">
{{ {{
formatPrice( formatPrice(
vintl.locale,
midasSubscriptionPrice.prices.intervals[midasSubscription.interval], midasSubscriptionPrice.prices.intervals[midasSubscription.interval],
midasSubscriptionPrice.currency_code, midasSubscriptionPrice.currency_code,
) )
@@ -60,7 +59,7 @@
{{ midasSubscription.interval }} {{ midasSubscription.interval }}
</template> </template>
<template v-else> <template v-else>
{{ formatPrice(vintl.locale, price.prices.intervals.monthly, price.currency_code) }} {{ formatPrice(price.prices.intervals.monthly, price.currency_code) }}
/ month / month
</template> </template>
</span> </span>
@@ -77,7 +76,7 @@
> >
<span class="opacity-70">Next:</span> <span class="opacity-70">Next:</span>
<span class="font-semibold text-contrast"> <span class="font-semibold text-contrast">
{{ formatPrice(vintl.locale, midasCharge.amount, midasCharge.currency_code) }} {{ formatPrice(midasCharge.amount, midasCharge.currency_code) }}
</span> </span>
<span>/{{ midasCharge.subscription_interval.replace('ly', '') }}</span> <span>/{{ midasCharge.subscription_interval.replace('ly', '') }}</span>
</div> </div>
@@ -90,21 +89,17 @@
> >
Save Save
{{ {{
formatPrice( formatPrice(midasCharge.amount * 12 - oppositePrice, midasCharge.currency_code)
vintl.locale,
midasCharge.amount * 12 - oppositePrice,
midasCharge.currency_code,
)
}}/year by switching to yearly billing! }}/year by switching to yearly billing!
</span> </span>
<span class="text-sm text-secondary"> <span class="text-sm text-secondary">
Since {{ $dayjs(midasSubscription.created).format('MMMM D, YYYY') }} Since {{ formatDate(midasSubscription.created) }}
</span> </span>
<span v-if="midasCharge.status === 'open'" class="text-sm text-secondary"> <span v-if="midasCharge.status === 'open'" class="text-sm text-secondary">
Renews {{ $dayjs(midasCharge.due).format('MMMM D, YYYY') }} Renews {{ formatDate(midasCharge.due) }}
</span> </span>
<span v-else-if="midasCharge.status === 'cancelled'" class="text-sm text-secondary"> <span v-else-if="midasCharge.status === 'cancelled'" class="text-sm text-secondary">
Expires {{ $dayjs(midasCharge.due).format('MMMM D, YYYY') }} Expires {{ formatDate(midasCharge.due) }}
</span> </span>
<span <span
v-if=" v-if="
@@ -116,14 +111,13 @@
class="text-sm text-secondary" class="text-sm text-secondary"
> >
Switches to {{ midasCharge.subscription_interval }} billing on Switches to {{ midasCharge.subscription_interval }} billing on
{{ $dayjs(midasCharge.due).format('MMMM D, YYYY') }} {{ formatDate(midasCharge.due) }}
</span> </span>
</template> </template>
<span v-else class="text-sm text-secondary"> <span v-else class="text-sm text-secondary">
Or Or
{{ formatPrice(vintl.locale, price.prices.intervals.yearly, price.currency_code) }} / {{ formatPrice(price.prices.intervals.yearly, price.currency_code) }} / year (save
year (save
{{ {{
calculateSavings(price.prices.intervals.monthly, price.prices.intervals.yearly) calculateSavings(price.prices.intervals.monthly, price.prices.intervals.yearly)
}}%)! }}%)!
@@ -188,7 +182,6 @@
v-tooltip=" v-tooltip="
midasCharge.subscription_interval === 'yearly' midasCharge.subscription_interval === 'yearly'
? `Monthly billing will cost you an additional ${formatPrice( ? `Monthly billing will cost you an additional ${formatPrice(
vintl.locale,
oppositePrice * 12 - midasCharge.amount, oppositePrice * 12 - midasCharge.amount,
midasCharge.currency_code, midasCharge.currency_code,
)} per year` )} per year`
@@ -307,7 +300,6 @@
<span class="text-contrast"> <span class="text-contrast">
{{ {{
formatPrice( formatPrice(
vintl.locale,
getProductPrice(getPyroProduct(subscription), subscription.interval) getProductPrice(getPyroProduct(subscription), subscription.interval)
.prices.intervals[subscription.interval], .prices.intervals[subscription.interval],
getProductPrice(getPyroProduct(subscription), subscription.interval) getProductPrice(getPyroProduct(subscription), subscription.interval)
@@ -333,7 +325,6 @@
<span class="font-semibold text-contrast"> <span class="font-semibold text-contrast">
{{ {{
formatPrice( formatPrice(
vintl.locale,
getPyroCharge(subscription).amount, getPyroCharge(subscription).amount,
getPyroCharge(subscription).currency_code, getPyroCharge(subscription).currency_code,
) )
@@ -351,13 +342,13 @@
</div> </div>
<div v-if="getPyroCharge(subscription)" class="mb-4 flex flex-col items-end"> <div v-if="getPyroCharge(subscription)" class="mb-4 flex flex-col items-end">
<span class="text-sm text-secondary"> <span class="text-sm text-secondary">
Since {{ $dayjs(subscription.created).format('MMMM D, YYYY') }} Since {{ formatDate(subscription.created) }}
</span> </span>
<span <span
v-if="getPyroCharge(subscription).status === 'open'" v-if="getPyroCharge(subscription).status === 'open'"
class="text-sm text-secondary" class="text-sm text-secondary"
> >
Renews {{ $dayjs(getPyroCharge(subscription).due).format('MMMM D, YYYY') }} Renews {{ formatDate(getPyroCharge(subscription).due) }}
</span> </span>
<span <span
v-if=" v-if="
@@ -371,7 +362,7 @@
Switches to Switches to
{{ getPyroCharge(subscription).subscription_interval }} {{ getPyroCharge(subscription).subscription_interval }}
billing on billing on
{{ $dayjs(getPyroCharge(subscription).due).format('MMMM D, YYYY') }} {{ formatDate(getPyroCharge(subscription).due) }}
</span> </span>
<span <span
v-else-if="getPyroCharge(subscription).status === 'processing'" v-else-if="getPyroCharge(subscription).status === 'processing'"
@@ -384,7 +375,7 @@
v-else-if="getPyroCharge(subscription).status === 'cancelled'" v-else-if="getPyroCharge(subscription).status === 'cancelled'"
class="text-sm text-secondary" class="text-sm text-secondary"
> >
Expires {{ $dayjs(getPyroCharge(subscription).due).format('MMMM D, YYYY') }} Expires {{ formatDate(getPyroCharge(subscription).due) }}
</span> </span>
<span <span
v-else-if="getPyroCharge(subscription).status === 'failed'" v-else-if="getPyroCharge(subscription).status === 'failed'"
@@ -624,9 +615,11 @@ import {
paymentMethodMessages, paymentMethodMessages,
PurchaseModal, PurchaseModal,
ServerListing, ServerListing,
useFormatDateTime,
useFormatPrice,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { calculateSavings, formatPrice, getCurrency } from '@modrinth/utils' import { calculateSavings, getCurrency } from '@modrinth/utils'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useBaseFetch } from '@/composables/fetch.js' import { useBaseFetch } from '@/composables/fetch.js'
@@ -655,8 +648,13 @@ useHead({
const config = useRuntimeConfig() const config = useRuntimeConfig()
const vintl = useVIntl() const { formatMessage } = useVIntl()
const { formatMessage } = vintl const formatPrice = useFormatPrice()
const formatDate = useFormatDateTime({
year: 'numeric',
month: 'long',
day: 'numeric',
})
const deleteModalMessages = defineMessages({ const deleteModalMessages = defineMessages({
title: { title: {

View File

@@ -119,16 +119,7 @@
<CopyCode :text="pat.access_token" /> <CopyCode :text="pat.access_token" />
</template> </template>
<template v-else> <template v-else>
<span <span v-tooltip="pat.last_used ? formatDateTime(pat.last_used) : null">
v-tooltip="
pat.last_used
? formatMessage(commonMessages.dateAtTimeTooltip, {
date: new Date(pat.last_used),
time: new Date(pat.last_used),
})
: null
"
>
<template v-if="pat.last_used"> <template v-if="pat.last_used">
{{ {{
formatMessage(tokenMessages.lastUsed, { formatMessage(tokenMessages.lastUsed, {
@@ -139,14 +130,7 @@
<template v-else>{{ formatMessage(tokenMessages.neverUsed) }}</template> <template v-else>{{ formatMessage(tokenMessages.neverUsed) }}</template>
</span> </span>
<span <span v-tooltip="formatDateTime(pat.expires)">
v-tooltip="
formatMessage(commonMessages.dateAtTimeTooltip, {
date: new Date(pat.expires),
time: new Date(pat.expires),
})
"
>
<template v-if="new Date(pat.expires) > new Date()"> <template v-if="new Date(pat.expires) > new Date()">
{{ {{
formatMessage(tokenMessages.expiresIn, { formatMessage(tokenMessages.expiresIn, {
@@ -163,14 +147,7 @@
</template> </template>
</span> </span>
<span <span v-tooltip="formatDateTime(pat.created)">
v-tooltip="
formatMessage(commonMessages.dateAtTimeTooltip, {
date: new Date(pat.created),
time: new Date(pat.created),
})
"
>
{{ {{
formatMessage(commonMessages.createdAgoLabel, { formatMessage(commonMessages.createdAgoLabel, {
ago: formatRelativeTime(pat.created), ago: formatRelativeTime(pat.created),
@@ -222,6 +199,7 @@ import {
injectNotificationManager, injectNotificationManager,
IntlFormatted, IntlFormatted,
StyledInput, StyledInput,
useFormatDateTime,
useRelativeTime, useRelativeTime,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
@@ -240,6 +218,10 @@ const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const createModalMessages = defineMessages({ const createModalMessages = defineMessages({
createTitle: { createTitle: {

View File

@@ -15,14 +15,7 @@
</div> </div>
<div> <div>
<template v-if="session.city">{{ session.city }}, {{ session.country }} </template> <template v-if="session.city">{{ session.city }}, {{ session.country }} </template>
<span <span v-tooltip="formatDateTime(session.last_login)">
v-tooltip="
formatMessage(commonMessages.dateAtTimeTooltip, {
date: new Date(session.last_login),
time: new Date(session.last_login),
})
"
>
{{ {{
formatMessage(messages.lastAccessedAgoLabel, { formatMessage(messages.lastAccessedAgoLabel, {
ago: formatRelativeTime(session.last_login), ago: formatRelativeTime(session.last_login),
@@ -30,14 +23,7 @@
}} }}
</span> </span>
<span <span v-tooltip="formatDateTime(session.created)">
v-tooltip="
formatMessage(commonMessages.dateAtTimeTooltip, {
date: new Date(session.created),
time: new Date(session.created),
})
"
>
{{ {{
formatMessage(messages.createdAgoLabel, { formatMessage(messages.createdAgoLabel, {
ago: formatRelativeTime(session.created), ago: formatRelativeTime(session.created),
@@ -62,6 +48,7 @@ import {
commonSettingsMessages, commonSettingsMessages,
defineMessages, defineMessages,
injectNotificationManager, injectNotificationManager,
useFormatDateTime,
useRelativeTime, useRelativeTime,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
@@ -73,6 +60,10 @@ definePageMeta({
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const messages = defineMessages({ const messages = defineMessages({
currentSessionLabel: { currentSessionLabel: {

View File

@@ -155,27 +155,24 @@
{{ {{
formatMessage(messages.profileProjectsLabel, { formatMessage(messages.profileProjectsLabel, {
count: formatCompactNumber(projects?.length || 0), count: formatCompactNumber(projects?.length || 0),
countPlural: formatCompactNumberPlural(projects?.length || 0),
}) })
}} }}
</div> </div>
<div <div
v-tooltip="sumDownloads.toLocaleString()" v-tooltip="formatNumber(sumDownloads)"
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold" class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
> >
<DownloadIcon class="h-6 w-6 text-secondary" /> <DownloadIcon class="h-6 w-6 text-secondary" />
{{ {{
formatMessage(messages.profileDownloadsLabel, { formatMessage(messages.profileDownloadsLabel, {
count: formatCompactNumber(sumDownloads), count: formatCompactNumber(sumDownloads),
countPlural: formatCompactNumberPlural(sumDownloads),
}) })
}} }}
</div> </div>
<div <div
v-tooltip=" v-tooltip="formatDateTime(user.created)"
formatMessage(commonMessages.dateAtTimeTooltip, {
date: new Date(user.created),
time: new Date(user.created),
})
"
class="flex items-center gap-2 font-semibold" class="flex items-center gap-2 font-semibold"
> >
<CalendarIcon class="h-6 w-6 text-secondary" /> <CalendarIcon class="h-6 w-6 text-secondary" />
@@ -502,6 +499,9 @@ import {
ProjectCard, ProjectCard,
ProjectCardList, ProjectCardList,
TagItem, TagItem,
useCompactNumber,
useFormatDateTime,
useFormatNumber,
useRelativeTime, useRelativeTime,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
@@ -528,12 +528,14 @@ const cosmetics = useCosmetics()
const tags = useGeneratedState() const tags = useGeneratedState()
const config = useRuntimeConfig() const config = useRuntimeConfig()
const vintl = useVIntl() const { formatMessage } = useVIntl()
const { formatMessage } = vintl const formatNumber = useFormatNumber()
const { formatCompactNumber, formatCompactNumberPlural } = useCompactNumber()
const formatCompactNumber = useCompactNumber(true)
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
@@ -542,11 +544,11 @@ const baseId = useId()
const messages = defineMessages({ const messages = defineMessages({
profileProjectsLabel: { profileProjectsLabel: {
id: 'profile.label.projects', id: 'profile.label.projects',
defaultMessage: '{count} {count, plural, one {project} other {projects}}', defaultMessage: '{count} {countPlural, plural, one {project} other {projects}}',
}, },
profileDownloadsLabel: { profileDownloadsLabel: {
id: 'profile.label.downloads', id: 'profile.label.downloads',
defaultMessage: '{count} {count, plural, one {download} other {downloads}}', defaultMessage: '{count} {countPlural, plural, one {download} other {downloads}}',
}, },
profileJoinedLabel: { profileJoinedLabel: {
id: 'profile.label.joined', id: 'profile.label.joined',

View File

@@ -70,6 +70,7 @@
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"intl-messageformat": "^10.7.7", "intl-messageformat": "^10.7.7",
"lru-cache": "^11.2.4",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"markdown-it": "^13.0.2", "markdown-it": "^13.0.2",
"postprocessing": "^6.37.6", "postprocessing": "^6.37.6",

View File

@@ -1,12 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { SpinnerIcon } from '@modrinth/assets' import { SpinnerIcon } from '@modrinth/assets'
import { formatPrice } from '@modrinth/utils'
import { computed } from 'vue' import { computed } from 'vue'
import { useVIntl } from '../../composables/i18n' import { useFormatPrice } from '../../composables'
import Accordion from '../base/Accordion.vue' import Accordion from '../base/Accordion.vue'
const { locale } = useVIntl() const formatPrice = useFormatPrice()
export type BillingItem = { export type BillingItem = {
title: string title: string
@@ -38,7 +37,7 @@ const periodSuffix = computed(() => {
<template v-if="loading"> <template v-if="loading">
<SpinnerIcon class="animate-spin size-4" /> <SpinnerIcon class="animate-spin size-4" />
</template> </template>
<template v-else> {{ formatPrice(locale, total, currency) }} </template <template v-else> {{ formatPrice(total, currency) }} </template
><span class="text-xs text-secondary">{{ periodSuffix }}</span> ><span class="text-xs text-secondary">{{ periodSuffix }}</span>
</span> </span>
</div> </div>
@@ -57,7 +56,7 @@ const periodSuffix = computed(() => {
<template v-if="loading"> <template v-if="loading">
<SpinnerIcon class="animate-spin size-4" /> <SpinnerIcon class="animate-spin size-4" />
</template> </template>
<template v-else> {{ formatPrice(locale, amount, currency) }} </template <template v-else> {{ formatPrice(amount, currency) }} </template
><span class="text-xs text-secondary">{{ periodSuffix }}</span> ><span class="text-xs text-secondary">{{ periodSuffix }}</span>
</div> </div>
</div> </div>

View File

@@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client' import type { Labrinth } from '@modrinth/api-client'
import { InfoIcon } from '@modrinth/assets' import { InfoIcon } from '@modrinth/assets'
import { formatPrice } from '@modrinth/utils'
import { Menu } from 'floating-vue' import { Menu } from 'floating-vue'
import { computed, inject, type Ref } from 'vue' import { computed, inject, type Ref } from 'vue'
import { useFormatPrice } from '../../composables'
import { type MessageDescriptor, useVIntl } from '../../composables/i18n' import { type MessageDescriptor, useVIntl } from '../../composables/i18n'
import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils' import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils'
import type { ServerBillingInterval } from './ModrinthServersPurchaseModal.vue' import type { ServerBillingInterval } from './ModrinthServersPurchaseModal.vue'
@@ -30,7 +30,8 @@ const emit = defineEmits<{
(e: 'select', plan: Labrinth.Billing.Internal.Product): void (e: 'select', plan: Labrinth.Billing.Internal.Product): void
}>() }>()
const { formatMessage, locale } = useVIntl() const { formatMessage } = useVIntl()
const formatPrice = useFormatPrice()
// TODO: Use DI framework when merged. // TODO: Use DI framework when merged.
const selectedInterval = inject<Ref<ServerBillingInterval>>('selectedInterval') const selectedInterval = inject<Ref<ServerBillingInterval>>('selectedInterval')
@@ -101,7 +102,7 @@ const mostPopularStyle = computed(() => {
</div> </div>
</div> </div>
<span class="m-0 text-lg font-bold text-contrast"> <span class="m-0 text-lg font-bold text-contrast">
{{ formatPrice(locale, perMonth, currency, true) }} {{ formatPrice(perMonth, currency, true) }}
<span class="text-sm font-semibold text-secondary"> <span class="text-sm font-semibold text-secondary">
/ month{{ selectedInterval !== 'monthly' ? `, billed ${selectedInterval}` : '' }} / month{{ selectedInterval !== 'monthly' ? `, billed ${selectedInterval}` : '' }}
</span> </span>

View File

@@ -232,7 +232,7 @@
}}% }}%
</span> </span>
<span class="ml-auto text-lg" :class="{ 'text-secondary': selectedPlan !== interval }"> <span class="ml-auto text-lg" :class="{ 'text-secondary': selectedPlan !== interval }">
{{ formatPrice(locale, rawPrice, price.currency_code) }} {{ formatPrice(rawPrice, price.currency_code) }}
</span> </span>
</div> </div>
</div> </div>
@@ -240,7 +240,7 @@
<span class="text-xl text-secondary">Total</span> <span class="text-xl text-secondary">Total</span>
<div class="flex items-baseline gap-2"> <div class="flex items-baseline gap-2">
<span class="text-2xl font-extrabold text-primary"> <span class="text-2xl font-extrabold text-primary">
{{ formatPrice(locale, price.prices.intervals[selectedPlan], price.currency_code) }} {{ formatPrice(price.prices.intervals[selectedPlan], price.currency_code) }}
</span> </span>
<span class="text-lg text-secondary">/ {{ selectedPlan }}</span> <span class="text-lg text-secondary">/ {{ selectedPlan }}</span>
</div> </div>
@@ -304,23 +304,21 @@
}} }}
</span> </span>
<span v-if="existingPlan" class="text-secondary text-end"> <span v-if="existingPlan" class="text-secondary text-end">
{{ formatPrice(locale, total - tax, price.currency_code) }} {{ formatPrice(total - tax, price.currency_code) }}
</span> </span>
<span v-else class="text-secondary text-end"> <span v-else class="text-secondary text-end">
{{ formatPrice(locale, total - tax, price.currency_code) }} / {{ formatPrice(total - tax, price.currency_code) }} /
{{ selectedPlan }} {{ selectedPlan }}
</span> </span>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-secondary">Tax</span> <span class="text-secondary">Tax</span>
<span class="text-secondary text-end">{{ <span class="text-secondary text-end">{{ formatPrice(tax, price.currency_code) }}</span>
formatPrice(locale, tax, price.currency_code)
}}</span>
</div> </div>
<div class="mt-4 flex justify-between border-0 border-t border-solid border-code-bg pt-4"> <div class="mt-4 flex justify-between border-0 border-t border-solid border-code-bg pt-4">
<span class="text-lg font-bold">Today's total</span> <span class="text-lg font-bold">Today's total</span>
<span class="text-lg font-extrabold text-primary text-end"> <span class="text-lg font-extrabold text-primary text-end">
{{ formatPrice(locale, total, price.currency_code) }} {{ formatPrice(total, price.currency_code) }}
</span> </span>
</div> </div>
</div> </div>
@@ -416,9 +414,9 @@
<strong>By clicking "Subscribe", you are purchasing a recurring subscription.</strong> <strong>By clicking "Subscribe", you are purchasing a recurring subscription.</strong>
<br /> <br />
You'll be charged You'll be charged
{{ formatPrice(locale, price.prices.intervals[selectedPlan], price.currency_code) }} {{ formatPrice(price.prices.intervals[selectedPlan], price.currency_code) }}
/ {{ selectedPlan }} plus applicable taxes starting / {{ selectedPlan }} plus applicable taxes starting
{{ existingPlan ? dayjs(renewalDate).format('MMMM D, YYYY') : 'today' }}, until you cancel. {{ existingPlan ? formatDate(renewalDate) : 'today' }}, until you cancel.
<br /> <br />
You can cancel anytime from your settings page. You can cancel anytime from your settings page.
</p> </p>
@@ -545,12 +543,13 @@ import {
UnknownIcon, UnknownIcon,
XIcon, XIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { calculateSavings, createStripeElements, formatPrice, getCurrency } from '@modrinth/utils' import { calculateSavings, createStripeElements, getCurrency } from '@modrinth/utils'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { computed, nextTick, reactive, ref, watch } from 'vue' import { computed, nextTick, reactive, ref, watch } from 'vue'
import { Multiselect } from 'vue-multiselect' import { Multiselect } from 'vue-multiselect'
import { useVIntl } from '../../composables/i18n' import { useVIntl } from '../../composables/i18n'
import { useFormatDateTime, useFormatPrice } from '../../composables/index.ts'
import { paymentMethodMessages } from '../../utils/common-messages' import { paymentMethodMessages } from '../../utils/common-messages'
import Admonition from '../base/Admonition.vue' import Admonition from '../base/Admonition.vue'
import Checkbox from '../base/Checkbox.vue' import Checkbox from '../base/Checkbox.vue'
@@ -560,7 +559,9 @@ import AnimatedLogo from '../brand/AnimatedLogo.vue'
import NewModal from '../modal/NewModal.vue' import NewModal from '../modal/NewModal.vue'
import LoaderIcon from '../servers/icons/LoaderIcon.vue' import LoaderIcon from '../servers/icons/LoaderIcon.vue'
const { locale, formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatPrice = useFormatPrice()
const formatDate = useFormatDateTime({ dateStyle: 'long' })
const props = defineProps({ const props = defineProps({
product: { product: {

View File

@@ -1,15 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client' import type { Labrinth } from '@modrinth/api-client'
import { formatPrice } from '@modrinth/utils'
import { computed, provide } from 'vue' import { computed, provide } from 'vue'
import { useFormatPrice } from '../../composables'
import { defineMessages, useVIntl } from '../../composables/i18n' import { defineMessages, useVIntl } from '../../composables/i18n'
import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils' import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils'
import OptionGroup from '../base/OptionGroup.vue' import OptionGroup from '../base/OptionGroup.vue'
import ModalBasedServerPlan from './ModalBasedServerPlan.vue' import ModalBasedServerPlan from './ModalBasedServerPlan.vue'
import type { ServerBillingInterval } from './ModrinthServersPurchaseModal.vue' import type { ServerBillingInterval } from './ModrinthServersPurchaseModal.vue'
const { formatMessage, locale } = useVIntl() const { formatMessage } = useVIntl()
const formatPrice = useFormatPrice()
const props = defineProps<{ const props = defineProps<{
availableProducts: Labrinth.Billing.Internal.Product[] availableProducts: Labrinth.Billing.Internal.Product[]
@@ -209,7 +210,7 @@ provide('selectedInterval', selectedInterval)
<span class="text-2xl font-semibold text-contrast">Custom</span> <span class="text-2xl font-semibold text-contrast">Custom</span>
</div> </div>
<span class="m-0 text-lg font-bold text-contrast"> <span class="m-0 text-lg font-bold text-contrast">
{{ formatPrice(locale, customStartingPrice, currency, true) }} {{ formatPrice(customStartingPrice, currency, true) }}
<span class="text-sm font-semibold text-secondary"> <span class="text-sm font-semibold text-secondary">
/ month<template v-if="selectedInterval !== 'monthly'" / month<template v-if="selectedInterval !== 'monthly'"
>, billed {{ selectedInterval }}</template >, billed {{ selectedInterval }}</template
@@ -221,7 +222,7 @@ provide('selectedInterval', selectedInterval)
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span v-if="customPricePerGb" class="text-sm text-secondary"> <span v-if="customPricePerGb" class="text-sm text-secondary">
From {{ formatPrice(locale, customPricePerGb, currency, true) }} / GB From {{ formatPrice(customPricePerGb, currency, true) }} / GB
</span> </span>
</div> </div>
</div> </div>

View File

@@ -3,7 +3,7 @@ import type { Archon, Labrinth } from '@modrinth/api-client'
import { InfoIcon, SpinnerIcon, XIcon } from '@modrinth/assets' import { InfoIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { formatPrice } from '../../../../utils' import { useFormatPrice } from '../../composables'
import { defineMessages, useVIntl } from '../../composables/i18n' import { defineMessages, useVIntl } from '../../composables/i18n'
import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils.ts' import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils.ts'
import { regionOverrides } from '../../utils/regions.ts' import { regionOverrides } from '../../utils/regions.ts'
@@ -14,7 +14,8 @@ import type { RegionPing, ServerBillingInterval } from './ModrinthServersPurchas
import ServersRegionButton from './ServersRegionButton.vue' import ServersRegionButton from './ServersRegionButton.vue'
import ServersSpecs from './ServersSpecs.vue' import ServersSpecs from './ServersSpecs.vue'
const { formatMessage, locale } = useVIntl() const { formatMessage } = useVIntl()
const formatPrice = useFormatPrice()
const props = defineProps<{ const props = defineProps<{
regions: Archon.Servers.v1.Region[] regions: Archon.Servers.v1.Region[]
@@ -283,7 +284,7 @@ onMounted(() => {
<Slider v-model="selectedRam" :min="minRam" :max="maxRam" :step="2" unit="GB" /> <Slider v-model="selectedRam" :min="minRam" :max="maxRam" :step="2" unit="GB" />
<p v-if="selectedPrice" class="mt-2 mb-0"> <p v-if="selectedPrice" class="mt-2 mb-0">
<span class="text-contrast text-lg font-bold" <span class="text-contrast text-lg font-bold"
>{{ formatPrice(locale, selectedPrice, currency, true) }} / month</span >{{ formatPrice(selectedPrice, currency, true) }} / month</span
><span v-if="interval !== 'monthly'">, billed {{ interval }}</span> ><span v-if="interval !== 'monthly'">, billed {{ interval }}</span>
</p> </p>
<div class="bg-bg rounded-xl p-4 mt-2 text-secondary h-14"> <div class="bg-bg rounded-xl p-4 mt-2 text-secondary h-14">

View File

@@ -10,11 +10,12 @@ import {
SpinnerIcon, SpinnerIcon,
XIcon, XIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { formatPrice, getPingLevel } from '@modrinth/utils' import { getPingLevel } from '@modrinth/utils'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import type Stripe from 'stripe' import type Stripe from 'stripe'
import { computed } from 'vue' import { computed } from 'vue'
import { useFormatPrice } from '../../composables'
import { useVIntl } from '../../composables/i18n' import { useVIntl } from '../../composables/i18n'
import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils' import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils'
import { regionOverrides } from '../../utils/regions' import { regionOverrides } from '../../utils/regions'
@@ -27,8 +28,8 @@ import FormattedPaymentMethod from './FormattedPaymentMethod.vue'
import type { ServerBillingInterval } from './ModrinthServersPurchaseModal.vue' import type { ServerBillingInterval } from './ModrinthServersPurchaseModal.vue'
import ServersSpecs from './ServersSpecs.vue' import ServersSpecs from './ServersSpecs.vue'
const vintl = useVIntl() const { formatMessage } = useVIntl()
const { locale, formatMessage } = vintl const formatPrice = useFormatPrice()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'changePaymentMethod' | 'reloadPaymentIntent'): void (e: 'changePaymentMethod' | 'reloadPaymentIntent'): void
@@ -246,7 +247,7 @@ function setInterval(newInterval: ServerBillingInterval) {
>Pay monthly</span >Pay monthly</span
> >
<span class="text-sm text-secondary flex items-center gap-1" <span class="text-sm text-secondary flex items-center gap-1"
>{{ formatPrice(locale, monthlyPrice, currency, true) }} / month</span >{{ formatPrice(monthlyPrice, currency, true) }} / month</span
> >
</div> </div>
</button> </button>
@@ -268,17 +269,10 @@ function setInterval(newInterval: ServerBillingInterval) {
>{{ interval === 'quarterly' ? 'Saving' : 'Save' }} 16%</span >{{ interval === 'quarterly' ? 'Saving' : 'Save' }} 16%</span
></span ></span
> >
<span class="text-sm text-secondary flex items-center gap-1" <span class="text-sm text-secondary flex items-center gap-1">
>{{ {{ formatPrice((quarterlyPrice ?? 0) / monthsInInterval['quarterly'], currency, true) }} /
formatPrice( month
locale, </span>
(quarterlyPrice ?? 0) / monthsInInterval['quarterly'],
currency,
true,
)
}}
/ month</span
>
</div> </div>
</button> </button>
</div> </div>
@@ -346,14 +340,14 @@ function setInterval(newInterval: ServerBillingInterval) {
Today, you will be charged a prorated amount for the remainder of your current billing cycle. Today, you will be charged a prorated amount for the remainder of your current billing cycle.
<br /> <br />
Your subscription will renew at Your subscription will renew at
{{ formatPrice(locale, selectedPlanPriceForInterval, currency) }} / {{ period }} plus {{ formatPrice(selectedPlanPriceForInterval, currency) }} / {{ period }} plus applicable taxes
applicable taxes at the end of your current billing interval, until you cancel. You can cancel at the end of your current billing interval, until you cancel. You can cancel anytime from
anytime from your settings page. your settings page.
</template> </template>
<template v-else> <template v-else>
You'll be charged You'll be charged
<SpinnerIcon v-if="loading" class="animate-spin relative top-0.5 mx-2" /><template v-else>{{ <SpinnerIcon v-if="loading" class="animate-spin relative top-0.5 mx-2" /><template v-else>{{
formatPrice(locale, total, currency) formatPrice(total, currency)
}}</template> }}</template>
every {{ period }} plus applicable taxes starting today, until you cancel. You can cancel every {{ period }} plus applicable taxes starting today, until you cancel. You can cancel
anytime from your settings page. anytime from your settings page.

View File

@@ -47,12 +47,19 @@ import type { VersionEntry } from '@modrinth/utils/changelog'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useRelativeTime } from '../../composables' import { useFormatDateTime, useRelativeTime } from '../../composables'
import { defineMessages, useVIntl } from '../../composables/i18n' import { defineMessages, useVIntl } from '../../composables/i18n'
import AutoLink from '../base/AutoLink.vue' import AutoLink from '../base/AutoLink.vue'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const formatDate = useFormatDateTime({
dateStyle: 'long',
})
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -71,10 +78,10 @@ const props = withDefaults(
const currentDate = ref(dayjs()) const currentDate = ref(dayjs())
const recent = computed(() => props.entry.date.isAfter(currentDate.value.subtract(1, 'week'))) const recent = computed(() => props.entry.date.isAfter(currentDate.value.subtract(1, 'week')))
const future = computed(() => props.entry.date.isAfter(currentDate.value)) const future = computed(() => props.entry.date.isAfter(currentDate.value))
const dateTooltip = computed(() => props.entry.date.format('MMMM D, YYYY [at] h:mm A')) const dateTooltip = computed(() => formatDateTime(props.entry.date.toDate()))
const relativeDate = computed(() => formatRelativeTime(props.entry.date.toISOString())) const relativeDate = computed(() => formatRelativeTime(props.entry.date.toDate()))
const longDate = computed(() => props.entry.date.format('MMMM D, YYYY')) const longDate = computed(() => formatDate(props.entry.date.toDate()))
const versionName = computed(() => props.entry.version ?? longDate.value) const versionName = computed(() => props.entry.version ?? longDate.value)
const messages = defineMessages({ const messages = defineMessages({

View File

@@ -1,13 +1,14 @@
<!-- eslint-disable no-console --> <!-- eslint-disable no-console -->
<script setup> <script setup>
import { formatNumber } from '@modrinth/utils'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { defineAsyncComponent, ref } from 'vue' import { defineAsyncComponent, ref } from 'vue'
import { useFormatNumber } from '../../composables/index.ts'
import Button from '../base/Button.vue' import Button from '../base/Button.vue'
import Checkbox from '../base/Checkbox.vue' import Checkbox from '../base/Checkbox.vue'
const VueApexCharts = defineAsyncComponent(() => import('vue3-apexcharts')) const VueApexCharts = defineAsyncComponent(() => import('vue3-apexcharts'))
const formatNumber = useFormatNumber()
const props = defineProps({ const props = defineProps({
name: { name: {
@@ -149,7 +150,7 @@ const chartOptions = ref({
!props.hideTotal !props.hideTotal
? `<div class="value"> ? `<div class="value">
${props.prefix} ${props.prefix}
${formatNumber(series.reduce((a, b) => a + b[dataPointIndex], 0).toString(), false)} ${formatNumber(series.reduce((a, b) => a + b[dataPointIndex], 0).toString())}
${props.suffix} ${props.suffix}
</div>` </div>`
: `` : ``
@@ -163,7 +164,7 @@ const chartOptions = ref({
</div> </div>
<div class="value"> <div class="value">
${props.prefix} ${props.prefix}
${formatNumber(value[dataPointIndex], false)} ${formatNumber(value[dataPointIndex])}
${props.suffix} ${props.suffix}
</div> </div>
</div>` </div>`

View File

@@ -1,13 +1,15 @@
<!-- eslint-disable eslint-comments/require-description --> <!-- eslint-disable eslint-comments/require-description -->
<script setup> <script setup>
import { formatNumber } from '@modrinth/utils'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { defineAsyncComponent, ref } from 'vue' import { defineAsyncComponent, ref } from 'vue'
import { useFormatNumber } from '../../composables/index.ts'
import Card from '../base/Card.vue' import Card from '../base/Card.vue'
const VueApexCharts = defineAsyncComponent(() => import('vue3-apexcharts')) const VueApexCharts = defineAsyncComponent(() => import('vue3-apexcharts'))
const formatNumber = useFormatNumber()
const props = defineProps({ const props = defineProps({
value: { value: {
type: String, type: String,
@@ -124,7 +126,7 @@ const chartOptions = ref({
</div> </div>
<div class="value"> <div class="value">
${props.prefix} ${props.prefix}
${formatNumber(value[dataPointIndex], false)} ${formatNumber(value[dataPointIndex])}
${props.suffix} ${props.suffix}
</div> </div>
</div>` </div>`

View File

@@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import { useFormatDateTime } from '../../composables'
import AutoLink from '../base/AutoLink.vue' import AutoLink from '../base/AutoLink.vue'
const formatDate = useFormatDateTime({ dateStyle: 'long' })
export interface Article { export interface Article {
path: string path: string
thumbnail: string thumbnail: string
@@ -34,7 +35,7 @@ defineProps<{
{{ article.summary }} {{ article.summary }}
</p> </p>
<div class="mt-auto text-sm text-secondary"> <div class="mt-auto text-sm text-secondary">
{{ dayjs(article.date).format('MMMM D, YYYY') }} {{ formatDate(article.date) }}
</div> </div>
</div> </div>
</article> </article>

View File

@@ -10,6 +10,7 @@ import {
import { computed, getCurrentInstance } from 'vue' import { computed, getCurrentInstance } from 'vue'
import type { RouteLocationRaw } from 'vue-router' import type { RouteLocationRaw } from 'vue-router'
import { useCompactNumber } from '../../composables'
import { useRelativeTime } from '../../composables/how-ago' import { useRelativeTime } from '../../composables/how-ago'
import { defineMessages, useVIntl } from '../../composables/i18n' import { defineMessages, useVIntl } from '../../composables/i18n'
import { commonMessages } from '../../utils/common-messages' import { commonMessages } from '../../utils/common-messages'
@@ -66,11 +67,7 @@ const hasContentListener = computed(() => typeof instance?.vnode.props?.onConten
const hasUnlinkListener = computed(() => typeof instance?.vnode.props?.onUnlink === 'function') const hasUnlinkListener = computed(() => typeof instance?.vnode.props?.onUnlink === 'function')
const formatTimeAgo = useRelativeTime() const formatTimeAgo = useRelativeTime()
const { formatCompactNumber } = useCompactNumber()
const formatCompact = (n: number | undefined) => {
if (n === undefined) return ''
return new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 2 }).format(n)
}
</script> </script>
<template> <template>
@@ -165,12 +162,12 @@ const formatCompact = (n: number | undefined) => {
<div class="flex flex-wrap items-center gap-3"> <div class="flex flex-wrap items-center gap-3">
<div v-if="project.downloads !== undefined" class="flex items-center gap-2 text-secondary"> <div v-if="project.downloads !== undefined" class="flex items-center gap-2 text-secondary">
<DownloadIcon class="size-5" /> <DownloadIcon class="size-5" />
<span class="font-medium">{{ formatCompact(project.downloads) }}</span> <span class="font-medium">{{ formatCompactNumber(project.downloads) }}</span>
</div> </div>
<div v-if="project.followers !== undefined" class="flex items-center gap-2 text-secondary"> <div v-if="project.followers !== undefined" class="flex items-center gap-2 text-secondary">
<HeartIcon class="size-5" /> <HeartIcon class="size-5" />
<span class="font-medium">{{ formatCompact(project.followers) }}</span> <span class="font-medium">{{ formatCompactNumber(project.followers) }}</span>
</div> </div>
<div v-if="categories?.length" class="flex flex-wrap gap-2"> <div v-if="categories?.length" class="flex flex-wrap gap-2">

View File

@@ -180,6 +180,7 @@ import {
import { capitalizeString, renderHighlightedString } from '@modrinth/utils' import { capitalizeString, renderHighlightedString } from '@modrinth/utils'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useFormatDateTime } from '../../../composables'
import { defineMessages, useVIntl } from '../../../composables/i18n' import { defineMessages, useVIntl } from '../../../composables/i18n'
import { commonMessages } from '../../../utils/common-messages' import { commonMessages } from '../../../utils/common-messages'
import Avatar from '../../base/Avatar.vue' import Avatar from '../../base/Avatar.vue'
@@ -188,6 +189,7 @@ import StyledInput from '../../base/StyledInput.vue'
import NewModal from '../../modal/NewModal.vue' import NewModal from '../../modal/NewModal.vue'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatDate = useFormatDateTime({ dateStyle: 'long' })
const messages = defineMessages({ const messages = defineMessages({
updateVersionHeader: { updateVersionHeader: {
@@ -341,11 +343,7 @@ function getBadgeClasses(version: Labrinth.Versions.v2.Version): string {
} }
function formatLongDate(dateString: string): string { function formatLongDate(dateString: string): string {
return new Date(dateString).toLocaleDateString('en-US', { return formatDate(new Date(dateString))
year: 'numeric',
month: 'long',
day: 'numeric',
})
} }
function formatLoaderGameVersion(version: Labrinth.Versions.v2.Version): string { function formatLoaderGameVersion(version: Labrinth.Versions.v2.Version): string {

View File

@@ -50,7 +50,7 @@ const messages = defineMessages({
}, },
searchPlaceholder: { searchPlaceholder: {
id: 'instances.modpack-content-modal.search-placeholder', id: 'instances.modpack-content-modal.search-placeholder',
defaultMessage: 'Search {count} projects', defaultMessage: 'Search {count, number} {count, plural, one {project} other {projects}}',
}, },
loading: { loading: {
id: 'instances.modpack-content-modal.loading', id: 'instances.modpack-content-modal.loading',
@@ -82,7 +82,7 @@ const messages = defineMessages({
}, },
selectedCount: { selectedCount: {
id: 'instances.modpack-content-modal.selected-count', id: 'instances.modpack-content-modal.selected-count',
defaultMessage: '{count} selected', defaultMessage: '{count, number} selected',
}, },
enable: { enable: {
id: 'instances.modpack-content-modal.enable', id: 'instances.modpack-content-modal.enable',

View File

@@ -27,20 +27,20 @@
v-tooltip=" v-tooltip="
capitalizeString( capitalizeString(
formatMessage(commonMessages.projectDownloads, { formatMessage(commonMessages.projectDownloads, {
count: formatNumber(project.downloads, false), count: project.downloads,
}), }),
) )
" "
class="flex items-center gap-2 font-semibold cursor-help" class="flex items-center gap-2 font-semibold cursor-help"
> >
<DownloadIcon class="h-6 w-6 text-secondary" /> <DownloadIcon class="h-6 w-6 text-secondary" />
{{ formatNumber(project.downloads) }} {{ formatCompactNumber(project.downloads) }}
</div> </div>
<div <div
v-tooltip=" v-tooltip="
capitalizeString( capitalizeString(
formatMessage(commonMessages.projectFollowers, { formatMessage(commonMessages.projectFollowers, {
count: formatNumber(project.followers, false), count: project.followers,
}), }),
) )
" "
@@ -49,7 +49,7 @@
> >
<HeartIcon class="h-6 w-6 text-secondary" /> <HeartIcon class="h-6 w-6 text-secondary" />
<span class="font-semibold"> <span class="font-semibold">
{{ formatNumber(project.followers) }} {{ formatCompactNumber(project.followers) }}
</span> </span>
</div> </div>
</template> </template>
@@ -74,11 +74,11 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client' import type { Labrinth } from '@modrinth/api-client'
import { DownloadIcon, HeartIcon } from '@modrinth/assets' import { DownloadIcon, HeartIcon } from '@modrinth/assets'
import { capitalizeString, formatNumber, type Project } from '@modrinth/utils' import { capitalizeString, type Project } from '@modrinth/utils'
import { computed } from 'vue' import { computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useVIntl } from '../../composables' import { useCompactNumber, useVIntl } from '../../composables'
import { commonMessages } from '../../utils' import { commonMessages } from '../../utils'
import Avatar from '../base/Avatar.vue' import Avatar from '../base/Avatar.vue'
import ContentPageHeader from '../base/ContentPageHeader.vue' import ContentPageHeader from '../base/ContentPageHeader.vue'
@@ -89,6 +89,7 @@ import ServerDetails from './server/ServerDetails.vue'
const router = useRouter() const router = useRouter()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const { formatCompactNumber } = useCompactNumber()
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{

View File

@@ -171,12 +171,7 @@
class="flex flex-col justify-center gap-1 max-sm:flex-row max-sm:justify-start max-sm:gap-3 xl:contents" class="flex flex-col justify-center gap-1 max-sm:flex-row max-sm:justify-start max-sm:gap-3 xl:contents"
> >
<div <div
v-tooltip=" v-tooltip="formatDateTime(version.date_published)"
formatMessage(commonMessages.dateAtTimeTooltip, {
date: new Date(version.date_published),
time: new Date(version.date_published),
})
"
class="z-[1] flex cursor-help items-center gap-1 text-nowrap font-medium xl:self-center" class="z-[1] flex cursor-help items-center gap-1 text-nowrap font-medium xl:self-center"
> >
<CalendarIcon class="xl:hidden" /> <CalendarIcon class="xl:hidden" />
@@ -186,7 +181,7 @@
class="pointer-events-none z-[1] flex items-center gap-1 font-medium xl:self-center" class="pointer-events-none z-[1] flex items-center gap-1 font-medium xl:self-center"
> >
<DownloadIcon class="xl:hidden" /> <DownloadIcon class="xl:hidden" />
{{ formatNumber(version.downloads) }} {{ formatCompactNumber(version.downloads) }}
</div> </div>
</div> </div>
</div> </div>
@@ -227,12 +222,13 @@ import {
FormattedTag, FormattedTag,
Pagination, Pagination,
TagItem, TagItem,
useCompactNumber,
useFormatDateTime,
VersionChannelIndicator, VersionChannelIndicator,
VersionFilterControl, VersionFilterControl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { import {
formatBytes, formatBytes,
formatNumber,
formatVersionsForDisplay, formatVersionsForDisplay,
type GameVersionTag, type GameVersionTag,
type Version, type Version,
@@ -242,11 +238,15 @@ import { useRoute, useRouter } from 'vue-router'
import { useRelativeTime } from '../../composables' import { useRelativeTime } from '../../composables'
import { useVIntl } from '../../composables/i18n' import { useVIntl } from '../../composables/i18n'
import { commonMessages } from '../../utils/common-messages'
import { getEnvironmentTags } from './settings/environment/environments' import { getEnvironmentTags } from './settings/environment/environments'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const { formatCompactNumber } = useCompactNumber()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
type VersionWithDisplayUrlEnding = Version & { type VersionWithDisplayUrlEnding = Version & {
displayUrlEnding: string displayUrlEnding: string

View File

@@ -36,10 +36,7 @@
{{ formatMessage(commonMessages.projectFollowers, { count: project.followers }) }} {{ formatMessage(commonMessages.projectFollowers, { count: project.followers }) }}
</div> </div>
</div> </div>
<div <div v-if="project.approved" v-tooltip="formatDateTime(project.approved)">
v-if="project.approved"
v-tooltip="dayjs(project.approved).format('MMMM D, YYYY [at] h:mm A')"
>
<CalendarIcon aria-hidden="true" /> <CalendarIcon aria-hidden="true" />
<div> <div>
{{ {{
@@ -49,7 +46,7 @@
}} }}
</div> </div>
</div> </div>
<div v-else v-tooltip="dayjs(project.published).format('MMMM D, YYYY [at] h:mm A')"> <div v-else v-tooltip="formatDateTime(project.published)">
<CalendarIcon aria-hidden="true" /> <CalendarIcon aria-hidden="true" />
<div> <div>
{{ {{
@@ -59,7 +56,7 @@
</div> </div>
<div <div
v-if="project.status === 'processing' && project.queued" v-if="project.status === 'processing' && project.queued"
v-tooltip="dayjs(project.queued).format('MMMM D, YYYY [at] h:mm A')" v-tooltip="formatDateTime(project.queued)"
> >
<ScaleIcon aria-hidden="true" /> <ScaleIcon aria-hidden="true" />
<div> <div>
@@ -70,10 +67,7 @@
}} }}
</div> </div>
</div> </div>
<div <div v-if="hasVersions && project.updated" v-tooltip="formatDateTime(project.updated)">
v-if="hasVersions && project.updated"
v-tooltip="dayjs(project.updated).format('MMMM D, YYYY [at] h:mm A')"
>
<VersionIcon aria-hidden="true" /> <VersionIcon aria-hidden="true" />
<div> <div>
{{ {{
@@ -95,16 +89,19 @@ import {
VersionIcon, VersionIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { capitalizeString } from '@modrinth/utils' import { capitalizeString } from '@modrinth/utils'
import dayjs from 'dayjs'
import { computed } from 'vue' import { computed } from 'vue'
import { useRelativeTime } from '../../composables' import { useFormatDateTime, useRelativeTime } from '../../composables'
import { defineMessages, useVIntl } from '../../composables/i18n' import { defineMessages, useVIntl } from '../../composables/i18n'
import { commonMessages } from '../../utils/common-messages' import { commonMessages } from '../../utils/common-messages'
import { IntlFormatted } from '../base' import { IntlFormatted } from '../base'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const props = defineProps<{ const props = defineProps<{
project: Labrinth.Projects.v2.Project project: Labrinth.Projects.v2.Project

View File

@@ -1,36 +1,39 @@
<script setup lang="ts"> <script setup lang="ts">
import { CalendarIcon, HistoryIcon } from '@modrinth/assets' import { CalendarIcon, HistoryIcon } from '@modrinth/assets'
import { capitalizeString } from '@modrinth/utils' import { capitalizeString } from '@modrinth/utils'
import dayjs from 'dayjs'
import { computed } from 'vue' import { computed } from 'vue'
import { useRelativeTime, useVIntl } from '../../../composables' import { defineMessage, useFormatDateTime, useRelativeTime, useVIntl } from '../../../composables'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime() const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const props = defineProps<{ const props = defineProps<{
date: Date date: Date
type: 'updated' | 'published' type: 'updated' | 'published'
}>() }>()
const formattedDate = computed(() => dayjs(props.date).format('MMMM D, YYYY [at] h:mm A')) const formattedDate = computed(() => formatDateTime(props.date))
const types = { const types = {
updated: { updated: {
icon: HistoryIcon, icon: HistoryIcon,
tooltip: { tooltip: defineMessage({
id: 'project-card.date.updated.tooltip', id: 'project-card.date.updated.tooltip',
defaultMessage: 'Updated {date}', defaultMessage: 'Updated {date}',
}, }),
}, },
published: { published: {
icon: CalendarIcon, icon: CalendarIcon,
tooltip: { tooltip: defineMessage({
id: 'project-card.date.published.tooltip', id: 'project-card.date.published.tooltip',
defaultMessage: 'Published {date}', defaultMessage: 'Published {date}',
}, }),
}, },
} }

View File

@@ -1,11 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { DownloadIcon, HeartIcon } from '@modrinth/assets' import { DownloadIcon, HeartIcon } from '@modrinth/assets'
import { capitalizeString, formatNumber } from '../../../../../utils' import { capitalizeString } from '../../../../../utils'
import { useVIntl } from '../../../composables' import { useCompactNumber, useVIntl } from '../../../composables'
import { commonMessages } from '../../../utils' import { commonMessages } from '../../../utils'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const { formatCompactNumber } = useCompactNumber()
defineProps<{ defineProps<{
downloads?: number downloads?: number
@@ -19,7 +20,7 @@ defineProps<{
v-tooltip=" v-tooltip="
capitalizeString( capitalizeString(
formatMessage(commonMessages.projectDownloads, { formatMessage(commonMessages.projectDownloads, {
count: formatNumber(downloads, false), count: downloads,
}), }),
) )
" "
@@ -27,7 +28,7 @@ defineProps<{
> >
<DownloadIcon class="size-5 shrink-0" /> <DownloadIcon class="size-5 shrink-0" />
<span class="font-medium"> <span class="font-medium">
{{ formatNumber(downloads) }} {{ formatCompactNumber(downloads) }}
</span> </span>
</div> </div>
<div <div
@@ -35,7 +36,7 @@ defineProps<{
v-tooltip=" v-tooltip="
capitalizeString( capitalizeString(
formatMessage(commonMessages.projectFollowers, { formatMessage(commonMessages.projectFollowers, {
count: formatNumber(followers, false), count: followers,
}), }),
) )
" "
@@ -43,7 +44,7 @@ defineProps<{
> >
<HeartIcon class="size-5 shrink-0" /> <HeartIcon class="size-5 shrink-0" />
<span class="font-medium"> <span class="font-medium">
{{ formatNumber(followers) }} {{ formatCompactNumber(followers) }}
</span> </span>
</div> </div>
</template> </template>

View File

@@ -1,12 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { OnlineIndicatorIcon } from '@modrinth/assets' import { OnlineIndicatorIcon } from '@modrinth/assets'
import { formatNumber } from '../../../../../utils' import { useCompactNumber, useFormatNumber, useVIntl } from '../../../composables'
import { useVIntl } from '../../../composables'
import { commonMessages } from '../../../utils' import { commonMessages } from '../../../utils'
import { StatItem } from '../../base' import { StatItem } from '../../base'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const { formatCompactNumber, formatCompactNumberPlural } = useCompactNumber()
const formatNumber = useFormatNumber()
defineProps<{ defineProps<{
online: number online: number
@@ -16,7 +17,12 @@ defineProps<{
</script> </script>
<template> <template>
<StatItem <StatItem
v-tooltip="`${formatNumber(online, true)} players online`" v-tooltip="
formatMessage(commonMessages.projectOnlinePlayerCountTooltip, {
count: formatCompactNumber(online),
countPlural: formatCompactNumberPlural(online),
})
"
class="smart-clickable:allow-pointer-events w-max" class="smart-clickable:allow-pointer-events w-max"
> >
<OnlineIndicatorIcon <OnlineIndicatorIcon
@@ -29,10 +35,8 @@ defineProps<{
/> />
{{ {{
hideLabel hideLabel
? formatNumber(online, false) ? formatNumber(online)
: formatMessage(commonMessages.projectOnlinePlayerCount, { : formatMessage(commonMessages.projectOnlinePlayerCount, { count: online })
count: formatNumber(online, false),
})
}} }}
</StatItem> </StatItem>
</template> </template>

View File

@@ -12,7 +12,7 @@ const props = defineProps<{
const pingMessage = defineMessage({ const pingMessage = defineMessage({
id: 'project.server.ping.ms', id: 'project.server.ping.ms',
defaultMessage: '{ping} ms', defaultMessage: '{ping, number} ms',
}) })
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()

View File

@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { PlayIcon } from '@modrinth/assets' import { PlayIcon } from '@modrinth/assets'
import { formatNumber } from '../../../../../utils' import { useCompactNumber, useVIntl } from '../../../composables'
import { useVIntl } from '../../../composables'
import { commonMessages } from '../../../utils' import { commonMessages } from '../../../utils'
import { StatItem } from '../../base' import { StatItem } from '../../base'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const { formatCompactNumber, formatCompactNumberPlural } = useCompactNumber()
defineProps<{ defineProps<{
recentPlays: number recentPlays: number
@@ -16,16 +16,20 @@ defineProps<{
<template> <template>
<StatItem <StatItem
v-tooltip=" v-tooltip="
`${formatNumber(recentPlays, true)} recent play${recentPlays === 1 ? '' : 's'} from Modrinth in the past 2 weeks` formatMessage(commonMessages.projectRecentPlaysTooltip, {
count: formatCompactNumber(recentPlays),
countPlural: formatCompactNumberPlural(recentPlays),
})
" "
class="smart-clickable:allow-pointer-events w-max" class="smart-clickable:allow-pointer-events w-max"
> >
<PlayIcon /> <PlayIcon />
{{ {{
hideLabel hideLabel
? formatNumber(recentPlays, true) ? formatCompactNumber(recentPlays)
: formatMessage(commonMessages.projectRecentPlays, { : formatMessage(commonMessages.projectRecentPlays, {
count: formatNumber(recentPlays, true), count: formatCompactNumber(recentPlays),
countPlural: formatCompactNumberPlural(recentPlays),
}) })
}} }}
</StatItem> </StatItem>

View File

@@ -122,9 +122,9 @@ import {
TriangleAlertIcon, TriangleAlertIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { useQuery } from '@tanstack/vue-query' import { useQuery } from '@tanstack/vue-query'
import dayjs from 'dayjs'
import { computed } from 'vue' import { computed } from 'vue'
import { useFormatDateTime } from '../../composables'
import { injectModrinthClient } from '../../providers/api-client' import { injectModrinthClient } from '../../providers/api-client'
import Avatar from '../base/Avatar.vue' import Avatar from '../base/Avatar.vue'
import CopyCode from '../base/CopyCode.vue' import CopyCode from '../base/CopyCode.vue'
@@ -132,6 +132,8 @@ import ServersSpecs from '../billing/ServersSpecs.vue'
import ServerIcon from './icons/ServerIcon.vue' import ServerIcon from './icons/ServerIcon.vue'
import ServerInfoLabels from './labels/ServerInfoLabels.vue' import ServerInfoLabels from './labels/ServerInfoLabels.vue'
const formatDate = useFormatDateTime({ dateStyle: 'long' })
export type PendingChange = { export type PendingChange = {
planSize: string planSize: string
cpu: number cpu: number
@@ -249,12 +251,4 @@ const { data: image } = useQuery({
}) })
const isConfiguring = computed(() => props.flows?.intro) const isConfiguring = computed(() => props.flows?.intro)
const formatDate = (d: unknown) => {
try {
return dayjs(d as string).format('MMMM D, YYYY')
} catch {
return ''
}
}
</script> </script>

View File

@@ -10,9 +10,9 @@ import {
UserRoundIcon, UserRoundIcon,
XIcon, XIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import dayjs from 'dayjs'
import { computed } from 'vue' import { computed } from 'vue'
import { useFormatDateTime } from '../../../composables'
import { defineMessages, useVIntl } from '../../../composables/i18n' import { defineMessages, useVIntl } from '../../../composables/i18n'
import { commonMessages } from '../../../utils' import { commonMessages } from '../../../utils'
import ButtonStyled from '../../base/ButtonStyled.vue' import ButtonStyled from '../../base/ButtonStyled.vue'
@@ -20,6 +20,10 @@ import OverflowMenu, { type Option as OverflowOption } from '../../base/Overflow
import ProgressBar from '../../base/ProgressBar.vue' import ProgressBar from '../../base/ProgressBar.vue'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'download' | 'rename' | 'restore' | 'retry'): void (e: 'download' | 'rename' | 'restore' | 'retry'): void
@@ -254,7 +258,7 @@ const messages = defineMessages({
</template> </template>
<template v-else> <template v-else>
<span class="w-full font-medium text-contrast md:text-center"> <span class="w-full font-medium text-contrast md:text-center">
{{ dayjs(backup.created_at).format('MMMM Do YYYY, h:mm A') }} {{ formatDateTime(backup.created_at) }}
</span> </span>
<!-- TODO: Uncomment when API supports size field --> <!-- TODO: Uncomment when API supports size field -->
<!-- <span class="text-secondary">{{ formatBytes(backup.size) }}</span> --> <!-- <span class="text-secondary">{{ formatBytes(backup.size) }}</span> -->

View File

@@ -81,6 +81,7 @@ import {
getFileExtensionIcon, getFileExtensionIcon,
isEditableFile as isEditableFileExt, isEditableFile as isEditableFileExt,
isImageFile, isImageFile,
useFormatDateTime,
} from '@modrinth/ui' } from '@modrinth/ui'
import { computed, ref, shallowRef } from 'vue' import { computed, ref, shallowRef } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
@@ -123,6 +124,14 @@ const units = Object.freeze(['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'])
const route = shallowRef(useRoute()) const route = shallowRef(useRoute())
const router = useRouter() const router = useRouter()
const formatDateTime = useFormatDateTime({
year: '2-digit',
month: '2-digit',
day: '2-digit',
hour: 'numeric',
minute: 'numeric',
})
const containerClasses = computed(() => [ const containerClasses = computed(() => [
'group m-0 flex w-full select-none items-center justify-between overflow-hidden border-0 border-t border-solid border-surface-3 px-4 py-3 focus:!outline-none', 'group m-0 flex w-full select-none items-center justify-between overflow-hidden border-0 border-t border-solid border-surface-3 px-4 py-3 focus:!outline-none',
props.selected ? 'bg-surface-3' : props.index % 2 === 0 ? 'bg-surface-2' : 'file-row-alt', props.selected ? 'bg-surface-3' : props.index % 2 === 0 ? 'bg-surface-2' : 'file-row-alt',
@@ -179,28 +188,12 @@ const iconComponent = computed(() => {
const formattedModifiedDate = computed(() => { const formattedModifiedDate = computed(() => {
const date = new Date(props.modified * 1000) const date = new Date(props.modified * 1000)
return `${date.toLocaleDateString('en-US', { return formatDateTime(date)
month: '2-digit',
day: '2-digit',
year: '2-digit',
})}, ${date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: 'numeric',
hour12: true,
})}`
}) })
const formattedCreationDate = computed(() => { const formattedCreationDate = computed(() => {
const date = new Date(props.created * 1000) const date = new Date(props.created * 1000)
return `${date.toLocaleDateString('en-US', { return formatDateTime(date)
month: '2-digit',
day: '2-digit',
year: '2-digit',
})}, ${date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: 'numeric',
hour12: true,
})}`
}) })
const isEditableFile = computed(() => { const isEditableFile = computed(() => {

View File

@@ -0,0 +1,38 @@
import { LRUCache } from 'lru-cache'
import { injectI18n } from '../providers/i18n'
const formatterCache = new LRUCache<string, Intl.DateTimeFormat>({ max: 40 })
export function useFormatDateTime(options?: Intl.DateTimeFormatOptions) {
const { locale } = injectI18n()
function format(date?: Date | number | string): string {
if (typeof date === 'number' || typeof date === 'string') {
date = new Date(date)
}
const formatter = getFormatter(locale.value, options)
return formatter!.format(date)
}
return format
}
function getFormatter(locale: string, options?: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {
let cacheKey = locale
if (options) {
const entries = Object.entries(options)
.filter(([, value]) => value !== undefined)
.sort()
.map(([key, value]) => `${key}=${value}`)
cacheKey = [locale, ...entries].join(':')
}
let formatter = formatterCache.get(cacheKey)
if (!formatter) {
formatter = new Intl.DateTimeFormat(locale, options)
formatterCache.set(cacheKey, formatter)
}
return formatter
}

View File

@@ -0,0 +1,78 @@
import { LRUCache } from 'lru-cache'
import { injectI18n } from '../providers/i18n'
const formatterCache = new LRUCache<string, Intl.NumberFormat>({ max: 10 })
const maxDigitsCache = new LRUCache<string, number>({ max: 10 })
// `formatMoney(1234.56, 'USD')` → `$1,234.56`
export function useFormatMoney() {
const { locale } = injectI18n()
function format(number: number, currency = 'USD'): string {
try {
const formatter = getFormatter(locale.value, currency)
return formatter!.format(number)
} catch {
return `${currency} ${number.toFixed(2)}`
}
}
return format
}
// `formatPrice(123456, 'USD')` → `$1,234.56`
export function useFormatPrice() {
const { locale } = injectI18n()
function format(price: number, currency: string, trimZeros = false): string {
const maxDigits = getMaxDigits(currency)
const convertedPrice = price / Math.pow(10, maxDigits)
const minimumFractionDigits = trimZeros && Number.isInteger(convertedPrice) ? 0 : undefined
try {
const formatter = getFormatter(locale.value, currency, minimumFractionDigits)
return formatter.format(convertedPrice)
} catch {
return `${currency} ${convertedPrice}`
}
}
return format
}
function getFormatter(
locale: string,
currency: string,
minimumFractionDigits?: number,
): Intl.NumberFormat {
const cacheKey = `${locale}:${currency}:${minimumFractionDigits}`
let formatter = formatterCache.get(cacheKey)
if (!formatter) {
formatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency,
minimumFractionDigits,
})
formatterCache.set(cacheKey, formatter)
}
return formatter
}
function getMaxDigits(currency: string): number {
let maxDigits = maxDigitsCache.get(currency)
if (!maxDigits) {
try {
const formatter = new Intl.NumberFormat(undefined, {
style: 'currency',
currency,
})
maxDigits = formatter.resolvedOptions().maximumFractionDigits || 2
} catch {
maxDigits = 2
}
maxDigitsCache.set(currency, maxDigits)
}
return maxDigits
}

View File

@@ -0,0 +1,74 @@
import { LRUCache } from 'lru-cache'
import { injectI18n } from '../providers/i18n'
const formatterCache = new LRUCache<string, Intl.NumberFormat>({ max: 15 })
// `formatNumber(1234567)` → `1,234,567`
export function useFormatNumber() {
const { locale } = injectI18n()
function format(value: number | bigint): string {
const formatter = getStandardFormatter(locale.value)
return formatter!.format(value)
}
return format
}
// `formatCompactNumber(1234567)` → `1.23M`
//
// Use `formatCompactNumberPlural` over `{(here!), plural, one {...} other {...}}`
export function useCompactNumber() {
const { locale } = injectI18n()
function formatCompactNumber(value: number | bigint): string {
if (value < 10_000) {
const standardFormatter = getStandardFormatter(locale.value)
return standardFormatter.format(value)
}
if (value < 1_000_000) {
const oneDigitCompactFormatter = getCompactFormatter(locale.value, 1)
return oneDigitCompactFormatter.format(value)
}
const twoDigitsCompactFormatter = getCompactFormatter(locale.value, 2)
return twoDigitsCompactFormatter.format(value)
}
function formatCompactNumberPlural(value: number | bigint): string {
if (value < 10_000) {
return value.toString()
}
if (value < 1_000_000) {
const oneDigitCompactFormatter = getCompactFormatter(locale.value, 1)
return oneDigitCompactFormatter.format(value)
}
const twoDigitsCompactFormatter = getCompactFormatter(locale.value, 2)
return twoDigitsCompactFormatter.format(value)
}
return { formatCompactNumber, formatCompactNumberPlural }
}
function getStandardFormatter(locale: string): Intl.NumberFormat {
const cacheKey = `${locale}:standard`
let formatter = formatterCache.get(cacheKey)
if (!formatter) {
formatter = new Intl.NumberFormat(locale)
formatterCache.set(cacheKey, formatter)
}
return formatter
}
function getCompactFormatter(locale: string, maximumFractionDigits: number): Intl.NumberFormat {
const cacheKey = `${locale}:compact:${maximumFractionDigits}`
let formatter = formatterCache.get(cacheKey)
if (!formatter) {
formatter = new Intl.NumberFormat(locale, {
notation: 'compact',
maximumFractionDigits,
})
formatterCache.set(cacheKey, formatter)
}
return formatter
}

View File

@@ -1,35 +1,14 @@
import { computed, type ComputedRef } from 'vue' import { LRUCache } from 'lru-cache'
import { injectI18n } from '../providers/i18n' import { injectI18n } from '../providers/i18n'
import { LOCALES } from './i18n.ts' import { LOCALES } from './i18n.ts'
export type Formatter = ( const formatterCache = new LRUCache<string, Intl.RelativeTimeFormat>({ max: 5 })
value: Date | number | null | string | undefined,
options?: FormatOptions,
) => string
export interface FormatOptions { export function useRelativeTime() {
roundingMode?: 'halfExpand' | 'floor' | 'ceil'
}
const formatters = new Map<string, ComputedRef<Intl.RelativeTimeFormat>>()
export function useRelativeTime(): Formatter {
const { locale } = injectI18n() const { locale } = injectI18n()
const formatterRef = computed(() => { return (value: Date | number | string | null | undefined) => {
const localeDefinition = LOCALES.find((loc) => loc.code === locale.value)
return new Intl.RelativeTimeFormat(locale.value, {
numeric: localeDefinition?.numeric || 'auto',
style: 'long',
})
})
if (!formatters.has(locale.value)) {
formatters.set(locale.value, formatterRef)
}
return (value: Date | number | null | string | undefined) => {
if (value == null) { if (value == null) {
return '' return ''
} }
@@ -50,7 +29,7 @@ export function useRelativeTime(): Formatter {
const months = Math.round(diff / 2629746000) const months = Math.round(diff / 2629746000)
const years = Math.round(diff / 31556952000) const years = Math.round(diff / 31556952000)
const rtf = formatterRef.value const rtf = getFormatter(locale.value)
if (Math.abs(seconds) < 60) { if (Math.abs(seconds) < 60) {
return rtf.format(seconds, 'second') return rtf.format(seconds, 'second')
@@ -69,3 +48,16 @@ export function useRelativeTime(): Formatter {
} }
} }
} }
function getFormatter(locale: string): Intl.RelativeTimeFormat {
let formatter = formatterCache.get(locale)
if (!formatter) {
const localeDefinition = LOCALES.find((loc) => loc.code === locale)
formatter = new Intl.RelativeTimeFormat(locale, {
numeric: localeDefinition?.numeric || 'auto',
style: 'long',
})
formatterCache.set(locale, formatter)
}
return formatter
}

View File

@@ -1,5 +1,8 @@
export * from './debug-logger' export * from './debug-logger'
export * from './dynamic-font-size' export * from './dynamic-font-size'
export * from './format-date-time'
export * from './format-money'
export * from './format-number'
export * from './how-ago' export * from './how-ago'
export * from './i18n' export * from './i18n'
export * from './i18n-debug' export * from './i18n-debug'

View File

@@ -360,10 +360,10 @@
"defaultMessage": "No projects match your search." "defaultMessage": "No projects match your search."
}, },
"instances.modpack-content-modal.search-placeholder": { "instances.modpack-content-modal.search-placeholder": {
"defaultMessage": "Search {count} projects" "defaultMessage": "Search {count, number} {count, plural, one {project} other {projects}}"
}, },
"instances.modpack-content-modal.selected-count": { "instances.modpack-content-modal.selected-count": {
"defaultMessage": "{count} selected" "defaultMessage": "{count, number} selected"
}, },
"instances.updater-modal.badge.current": { "instances.updater-modal.badge.current": {
"defaultMessage": "Current" "defaultMessage": "Current"
@@ -818,6 +818,12 @@
"payment-method.visa": { "payment-method.visa": {
"defaultMessage": "Visa" "defaultMessage": "Visa"
}, },
"project-card.date.published.tooltip": {
"defaultMessage": "Published {date}"
},
"project-card.date.updated.tooltip": {
"defaultMessage": "Updated {date}"
},
"project-card.environment.client": { "project-card.environment.client": {
"defaultMessage": "Client" "defaultMessage": "Client"
}, },
@@ -1008,7 +1014,7 @@
"defaultMessage": "Server details" "defaultMessage": "Server details"
}, },
"project.download-count-tooltip": { "project.download-count-tooltip": {
"defaultMessage": "{count} {count, plural, one {download} other {downloads}}" "defaultMessage": "{count, number} {count, plural, one {download} other {downloads}}"
}, },
"project.environment.client-and-server.description": { "project.environment.client-and-server.description": {
"defaultMessage": "Required on both the client and server." "defaultMessage": "Required on both the client and server."
@@ -1077,16 +1083,22 @@
"defaultMessage": "Unknown environment" "defaultMessage": "Unknown environment"
}, },
"project.follower-count-tooltip": { "project.follower-count-tooltip": {
"defaultMessage": "{count} {count, plural, one {follower} other {followers}}" "defaultMessage": "{count, number} {count, plural, one {follower} other {followers}}"
}, },
"project.online-player-count": { "project.online-player-count": {
"defaultMessage": "{count} {count, plural, one {online} other {online}}" "defaultMessage": "{count, number} online"
},
"project.online-player-count.tooltip": {
"defaultMessage": "{count} {countPlural, plural, one {player} other {players}} online"
}, },
"project.recent-plays": { "project.recent-plays": {
"defaultMessage": "{count} {count, plural, one {recent play} other {recent plays}}" "defaultMessage": "{count} {countPlural, plural, one {recent play} other {recent plays}}"
},
"project.recent-plays.tooltip": {
"defaultMessage": "{count} {countPlural, plural, one {recent play} other {recent plays}} from Modrinth in the past 2 weeks"
}, },
"project.server.ping.ms": { "project.server.ping.ms": {
"defaultMessage": "{ping} ms" "defaultMessage": "{ping, number} ms"
}, },
"project.settings.analytics.title": { "project.settings.analytics.title": {
"defaultMessage": "Analytics" "defaultMessage": "Analytics"
@@ -1958,9 +1970,6 @@
"tag.loader.waterfall": { "tag.loader.waterfall": {
"defaultMessage": "Waterfall" "defaultMessage": "Waterfall"
}, },
"tooltip.date-at-time": {
"defaultMessage": "{date, date, long} at {time, time, short}"
},
"ui.component.unsaved-changes-popup.body": { "ui.component.unsaved-changes-popup.body": {
"defaultMessage": "You have unsaved changes." "defaultMessage": "You have unsaved changes."
} }

View File

@@ -77,10 +77,6 @@ export const commonMessages = defineMessages({
id: 'label.dashboard', id: 'label.dashboard',
defaultMessage: 'Dashboard', defaultMessage: 'Dashboard',
}, },
dateAtTimeTooltip: {
id: 'tooltip.date-at-time',
defaultMessage: '{date, date, long} at {time, time, short}',
},
declineButton: { declineButton: {
id: 'button.decline', id: 'button.decline',
defaultMessage: 'Decline', defaultMessage: 'Decline',
@@ -387,19 +383,28 @@ export const commonMessages = defineMessages({
}, },
projectDownloads: { projectDownloads: {
id: 'project.download-count-tooltip', id: 'project.download-count-tooltip',
defaultMessage: '{count} {count, plural, one {download} other {downloads}}', defaultMessage: '{count, number} {count, plural, one {download} other {downloads}}',
}, },
projectFollowers: { projectFollowers: {
id: 'project.follower-count-tooltip', id: 'project.follower-count-tooltip',
defaultMessage: '{count} {count, plural, one {follower} other {followers}}', defaultMessage: '{count, number} {count, plural, one {follower} other {followers}}',
}, },
projectOnlinePlayerCount: { projectOnlinePlayerCount: {
id: 'project.online-player-count', id: 'project.online-player-count',
defaultMessage: '{count} {count, plural, one {online} other {online}}', defaultMessage: '{count, number} online',
},
projectOnlinePlayerCountTooltip: {
id: 'project.online-player-count.tooltip',
defaultMessage: '{count} {countPlural, plural, one {player} other {players}} online',
}, },
projectRecentPlays: { projectRecentPlays: {
id: 'project.recent-plays', id: 'project.recent-plays',
defaultMessage: '{count} {count, plural, one {recent play} other {recent plays}}', defaultMessage: '{count} {countPlural, plural, one {recent play} other {recent plays}}',
},
projectRecentPlaysTooltip: {
id: 'project.recent-plays.tooltip',
defaultMessage:
'{count} {countPlural, plural, one {recent play} other {recent plays}} from Modrinth in the past 2 weeks',
}, },
}) })

View File

@@ -60,29 +60,6 @@ export const getCurrency = (userCountry) => {
return countryCurrency[userCountry] ?? 'USD' return countryCurrency[userCountry] ?? 'USD'
} }
export const formatPrice = (locale, price, currency, trimZeros = false) => {
let formatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency,
})
const maxDigits = formatter.resolvedOptions().maximumFractionDigits
const convertedPrice = price / Math.pow(10, maxDigits)
let minimumFractionDigits = maxDigits
if (trimZeros && Number.isInteger(convertedPrice)) {
minimumFractionDigits = 0
}
formatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency,
minimumFractionDigits,
})
return formatter.format(convertedPrice)
}
export const calculateSavings = (monthlyPlan, plan, months = 12) => { export const calculateSavings = (monthlyPlan, plan, months = 12) => {
const monthlyAnnualized = monthlyPlan * months const monthlyAnnualized = monthlyPlan * months

View File

@@ -93,40 +93,6 @@ export const sortedCategories = (tags, formatCategoryName, locale) => {
}) })
} }
export const formatNumber = (number, abbreviate = true) => {
const x = Number(number)
if (x >= 1000000 && abbreviate) {
return `${(x / 1000000).toFixed(2).toString()}M`
} else if (x >= 10000 && abbreviate) {
return `${(x / 1000).toFixed(1).toString()}k`
}
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
export function formatDate(
date: dayjs.Dayjs,
options: Intl.DateTimeFormatOptions = {
month: 'long',
day: 'numeric',
year: 'numeric',
},
): string {
return date.toDate().toLocaleDateString(undefined, options)
}
export function formatMoney(number, abbreviate = false) {
const x = Number(number)
if (x >= 1000000 && abbreviate) {
return `$${(x / 1000000).toFixed(2).toString()}M`
} else if (x >= 10000 && abbreviate) {
return `$${(x / 1000).toFixed(2).toString()}k`
}
return `$${x
.toFixed(2)
.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`
}
export const formatBytes = (bytes, decimals = 2) => { export const formatBytes = (bytes, decimals = 2) => {
if (bytes === 0) return '0 Bytes' if (bytes === 0) return '0 Bytes'

10
pnpm-lock.yaml generated
View File

@@ -652,6 +652,9 @@ importers:
jszip: jszip:
specifier: ^3.10.1 specifier: ^3.10.1
version: 3.10.1 version: 3.10.1
lru-cache:
specifier: ^11.2.4
version: 11.2.5
markdown-it: markdown-it:
specifier: ^13.0.2 specifier: ^13.0.2
version: 13.0.2 version: 13.0.2
@@ -9401,6 +9404,9 @@ packages:
vue-component-type-helpers@3.2.4: vue-component-type-helpers@3.2.4:
resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==} resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==}
vue-component-type-helpers@3.2.5:
resolution: {integrity: sha512-tkvNr+bU8+xD/onAThIe7CHFvOJ/BO6XCOrxMzeytJq40nTfpGDJuVjyCM8ccGZKfAbGk2YfuZyDMXM56qheZQ==}
vue-confetti-explosion@1.0.2: vue-confetti-explosion@1.0.2:
resolution: {integrity: sha512-80OboM3/6BItIoZ6DpNcZFqGpF607kjIVc5af56oKgtFmt5yWehvJeoYhkzYlqxrqdBe0Ko4Ie3bWrmLau+dJw==} resolution: {integrity: sha512-80OboM3/6BItIoZ6DpNcZFqGpF607kjIVc5af56oKgtFmt5yWehvJeoYhkzYlqxrqdBe0Ko4Ie3bWrmLau+dJw==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -12755,7 +12761,7 @@ snapshots:
storybook: 10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) storybook: 10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
type-fest: 2.19.0 type-fest: 2.19.0
vue: 3.5.27(typescript@5.9.3) vue: 3.5.27(typescript@5.9.3)
vue-component-type-helpers: 3.2.4 vue-component-type-helpers: 3.2.5
'@stripe/stripe-js@7.9.0': {} '@stripe/stripe-js@7.9.0': {}
@@ -19520,6 +19526,8 @@ snapshots:
vue-component-type-helpers@3.2.4: {} vue-component-type-helpers@3.2.4: {}
vue-component-type-helpers@3.2.5: {}
vue-confetti-explosion@1.0.2(vue@3.5.27(typescript@5.9.3)): vue-confetti-explosion@1.0.2(vue@3.5.27(typescript@5.9.3)):
dependencies: dependencies:
vue: 3.5.27(typescript@5.9.3) vue: 3.5.27(typescript@5.9.3)