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-09 21:29:32 +00:00
committed by GitHub
co-authored by Calum H. Calum H.
parent 9b2f0c88cd
commit f62c60a681
88 changed files with 839 additions and 621 deletions
@@ -1,12 +1,11 @@
<script setup lang="ts">
import { SpinnerIcon } from '@modrinth/assets'
import { formatPrice } from '@modrinth/utils'
import { computed } from 'vue'
import { useVIntl } from '../../composables/i18n'
import { useFormatPrice } from '../../composables'
import Accordion from '../base/Accordion.vue'
const { locale } = useVIntl()
const formatPrice = useFormatPrice()
export type BillingItem = {
title: string
@@ -38,7 +37,7 @@ const periodSuffix = computed(() => {
<template v-if="loading">
<SpinnerIcon class="animate-spin size-4" />
</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>
</div>
@@ -57,7 +56,7 @@ const periodSuffix = computed(() => {
<template v-if="loading">
<SpinnerIcon class="animate-spin size-4" />
</template>
<template v-else> {{ formatPrice(locale, amount, currency) }} </template
<template v-else> {{ formatPrice(amount, currency) }} </template
><span class="text-xs text-secondary">{{ periodSuffix }}</span>
</div>
</div>
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { InfoIcon } from '@modrinth/assets'
import { formatPrice } from '@modrinth/utils'
import { Menu } from 'floating-vue'
import { computed, inject, type Ref } from 'vue'
import { useFormatPrice } from '../../composables'
import { type MessageDescriptor, useVIntl } from '../../composables/i18n'
import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils'
import type { ServerBillingInterval } from './ModrinthServersPurchaseModal.vue'
@@ -30,7 +30,8 @@ const emit = defineEmits<{
(e: 'select', plan: Labrinth.Billing.Internal.Product): void
}>()
const { formatMessage, locale } = useVIntl()
const { formatMessage } = useVIntl()
const formatPrice = useFormatPrice()
// TODO: Use DI framework when merged.
const selectedInterval = inject<Ref<ServerBillingInterval>>('selectedInterval')
@@ -101,7 +102,7 @@ const mostPopularStyle = computed(() => {
</div>
</div>
<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">
/ month{{ selectedInterval !== 'monthly' ? `, billed ${selectedInterval}` : '' }}
</span>
@@ -232,7 +232,7 @@
}}%
</span>
<span class="ml-auto text-lg" :class="{ 'text-secondary': selectedPlan !== interval }">
{{ formatPrice(locale, rawPrice, price.currency_code) }}
{{ formatPrice(rawPrice, price.currency_code) }}
</span>
</div>
</div>
@@ -240,7 +240,7 @@
<span class="text-xl text-secondary">Total</span>
<div class="flex items-baseline gap-2">
<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 class="text-lg text-secondary">/ {{ selectedPlan }}</span>
</div>
@@ -304,23 +304,21 @@
}}
</span>
<span v-if="existingPlan" class="text-secondary text-end">
{{ formatPrice(locale, total - tax, price.currency_code) }}
{{ formatPrice(total - tax, price.currency_code) }}
</span>
<span v-else class="text-secondary text-end">
{{ formatPrice(locale, total - tax, price.currency_code) }} /
{{ formatPrice(total - tax, price.currency_code) }} /
{{ selectedPlan }}
</span>
</div>
<div class="flex justify-between">
<span class="text-secondary">Tax</span>
<span class="text-secondary text-end">{{
formatPrice(locale, tax, price.currency_code)
}}</span>
<span class="text-secondary text-end">{{ formatPrice(tax, price.currency_code) }}</span>
</div>
<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-extrabold text-primary text-end">
{{ formatPrice(locale, total, price.currency_code) }}
{{ formatPrice(total, price.currency_code) }}
</span>
</div>
</div>
@@ -416,9 +414,9 @@
<strong>By clicking "Subscribe", you are purchasing a recurring subscription.</strong>
<br />
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
{{ existingPlan ? dayjs(renewalDate).format('MMMM D, YYYY') : 'today' }}, until you cancel.
{{ existingPlan ? formatDate(renewalDate) : 'today' }}, until you cancel.
<br />
You can cancel anytime from your settings page.
</p>
@@ -545,12 +543,13 @@ import {
UnknownIcon,
XIcon,
} from '@modrinth/assets'
import { calculateSavings, createStripeElements, formatPrice, getCurrency } from '@modrinth/utils'
import { calculateSavings, createStripeElements, getCurrency } from '@modrinth/utils'
import dayjs from 'dayjs'
import { computed, nextTick, reactive, ref, watch } from 'vue'
import { Multiselect } from 'vue-multiselect'
import { useVIntl } from '../../composables/i18n'
import { useFormatDateTime, useFormatPrice } from '../../composables/index.ts'
import { paymentMethodMessages } from '../../utils/common-messages'
import Admonition from '../base/Admonition.vue'
import Checkbox from '../base/Checkbox.vue'
@@ -560,7 +559,9 @@ import AnimatedLogo from '../brand/AnimatedLogo.vue'
import NewModal from '../modal/NewModal.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({
product: {
@@ -1,15 +1,16 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { formatPrice } from '@modrinth/utils'
import { computed, provide } from 'vue'
import { useFormatPrice } from '../../composables'
import { defineMessages, useVIntl } from '../../composables/i18n'
import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils'
import OptionGroup from '../base/OptionGroup.vue'
import ModalBasedServerPlan from './ModalBasedServerPlan.vue'
import type { ServerBillingInterval } from './ModrinthServersPurchaseModal.vue'
const { formatMessage, locale } = useVIntl()
const { formatMessage } = useVIntl()
const formatPrice = useFormatPrice()
const props = defineProps<{
availableProducts: Labrinth.Billing.Internal.Product[]
@@ -209,7 +210,7 @@ provide('selectedInterval', selectedInterval)
<span class="text-2xl font-semibold text-contrast">Custom</span>
</div>
<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">
/ month<template v-if="selectedInterval !== 'monthly'"
>, billed {{ selectedInterval }}</template
@@ -221,7 +222,7 @@ provide('selectedInterval', selectedInterval)
<div class="flex flex-col gap-2">
<div class="flex items-center gap-3">
<span v-if="customPricePerGb" class="text-sm text-secondary">
From {{ formatPrice(locale, customPricePerGb, currency, true) }} / GB
From {{ formatPrice(customPricePerGb, currency, true) }} / GB
</span>
</div>
</div>
@@ -3,7 +3,7 @@ import type { Archon, Labrinth } from '@modrinth/api-client'
import { InfoIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import { computed, onMounted, ref, watch } from 'vue'
import { formatPrice } from '../../../../utils'
import { useFormatPrice } from '../../composables'
import { defineMessages, useVIntl } from '../../composables/i18n'
import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils.ts'
import { regionOverrides } from '../../utils/regions.ts'
@@ -14,7 +14,8 @@ import type { RegionPing, ServerBillingInterval } from './ModrinthServersPurchas
import ServersRegionButton from './ServersRegionButton.vue'
import ServersSpecs from './ServersSpecs.vue'
const { formatMessage, locale } = useVIntl()
const { formatMessage } = useVIntl()
const formatPrice = useFormatPrice()
const props = defineProps<{
regions: Archon.Servers.v1.Region[]
@@ -283,7 +284,7 @@ onMounted(() => {
<Slider v-model="selectedRam" :min="minRam" :max="maxRam" :step="2" unit="GB" />
<p v-if="selectedPrice" class="mt-2 mb-0">
<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>
</p>
<div class="bg-bg rounded-xl p-4 mt-2 text-secondary h-14">
@@ -10,11 +10,12 @@ import {
SpinnerIcon,
XIcon,
} from '@modrinth/assets'
import { formatPrice, getPingLevel } from '@modrinth/utils'
import { getPingLevel } from '@modrinth/utils'
import dayjs from 'dayjs'
import type Stripe from 'stripe'
import { computed } from 'vue'
import { useFormatPrice } from '../../composables'
import { useVIntl } from '../../composables/i18n'
import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils'
import { regionOverrides } from '../../utils/regions'
@@ -27,8 +28,8 @@ import FormattedPaymentMethod from './FormattedPaymentMethod.vue'
import type { ServerBillingInterval } from './ModrinthServersPurchaseModal.vue'
import ServersSpecs from './ServersSpecs.vue'
const vintl = useVIntl()
const { locale, formatMessage } = vintl
const { formatMessage } = useVIntl()
const formatPrice = useFormatPrice()
const emit = defineEmits<{
(e: 'changePaymentMethod' | 'reloadPaymentIntent'): void
@@ -246,7 +247,7 @@ function setInterval(newInterval: ServerBillingInterval) {
>Pay monthly</span
>
<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>
</button>
@@ -268,17 +269,10 @@ function setInterval(newInterval: ServerBillingInterval) {
>{{ interval === 'quarterly' ? 'Saving' : 'Save' }} 16%</span
></span
>
<span class="text-sm text-secondary flex items-center gap-1"
>{{
formatPrice(
locale,
(quarterlyPrice ?? 0) / monthsInInterval['quarterly'],
currency,
true,
)
}}
/ month</span
>
<span class="text-sm text-secondary flex items-center gap-1">
{{ formatPrice((quarterlyPrice ?? 0) / monthsInInterval['quarterly'], currency, true) }} /
month
</span>
</div>
</button>
</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.
<br />
Your subscription will renew at
{{ formatPrice(locale, selectedPlanPriceForInterval, currency) }} / {{ period }} plus
applicable taxes at the end of your current billing interval, until you cancel. You can cancel
anytime from your settings page.
{{ formatPrice(selectedPlanPriceForInterval, currency) }} / {{ period }} plus applicable taxes
at the end of your current billing interval, until you cancel. You can cancel anytime from
your settings page.
</template>
<template v-else>
You'll be charged
<SpinnerIcon v-if="loading" class="animate-spin relative top-0.5 mx-2" /><template v-else>{{
formatPrice(locale, total, currency)
formatPrice(total, currency)
}}</template>
every {{ period }} plus applicable taxes starting today, until you cancel. You can cancel
anytime from your settings page.