devex: migrate to vue-i18n (#4966)

* sample languages refactor

* feat: consistency + dedupe impl of i18n

* fix: broken imports

* fix: intl formatted component

* fix: use relative imports

* fix: imports

* fix: comment out incomplete locales + fix imports

* feat: cleanup

* fix: ui imports

* fix: lint

* fix: admonition import

* make footer a component, fix language reactivity

* make copyright notice untranslatable

---------

Co-authored-by: Calum H. <contact@cal.engineer>
This commit is contained in:
Prospector
2025-12-27 13:37:37 -08:00
committed by GitHub
parent 3cabc3b967
commit 1bbb01bd42
161 changed files with 1449 additions and 2314 deletions

View File

@@ -33,9 +33,9 @@
<script setup lang="ts">
import { AffiliateIcon, XCircleIcon } from '@modrinth/assets'
import type { AffiliateLink } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { AutoBrandIcon, ButtonStyled, CopyCode } from '../index.ts'
import { defineMessages, useVIntl } from '../../composables/i18n'
import { AutoBrandIcon, ButtonStyled, CopyCode } from '../index'
withDefaults(
defineProps<{

View File

@@ -64,10 +64,10 @@
<script lang="ts"></script>
<script setup lang="ts">
import { AffiliateIcon, PlusIcon, SpinnerIcon, UserIcon, XIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { computed, ref, useTemplateRef } from 'vue'
import { AutoBrandIcon, Button, ButtonStyled, NewModal } from '../index.ts'
import { defineMessages, useVIntl } from '../../composables/i18n'
import { AutoBrandIcon, Button, ButtonStyled, NewModal } from '../index'
export type CreateAffiliateProps = { sourceName: string; username?: string }
const props = withDefaults(

View File

@@ -100,7 +100,8 @@ import {
XIcon,
} from '@modrinth/assets'
import { capitalizeString } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { defineMessages, useVIntl } from '../../composables/i18n'
const messages = defineMessages({
acceptedLabel: {

View File

@@ -8,9 +8,10 @@
<script setup lang="ts">
import { CheckIcon, ClipboardCopyIcon } from '@modrinth/assets'
import { defineMessage, useVIntl } from '@vintl/vintl'
import { ref } from 'vue'
import { defineMessage, useVIntl } from '../../composables/i18n'
const copiedMessage = defineMessage({
id: 'omorphia.component.copy.action.copy',
defaultMessage: 'Copy code to clipboard',

View File

@@ -49,7 +49,8 @@
</template>
<script setup lang="ts">
import { ClientIcon, GlobeIcon, InfoIcon, ServerIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { defineMessages, useVIntl } from '../../composables/i18n'
const messages = defineMessages({
clientLabel: {

View File

@@ -17,9 +17,10 @@
<script setup lang="ts">
import { FilterIcon } from '@modrinth/assets'
import { type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { watch } from 'vue'
import { type MessageDescriptor, useVIntl } from '../../composables/i18n'
const { formatMessage } = useVIntl()
export type FilterBarOption = {

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { EditIcon, TrashIcon, UploadIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { defineMessages, useVIntl } from '../../composables/i18n'
import { Avatar, OverflowMenu } from '../index'
const { formatMessage } = useVIntl()

View File

@@ -0,0 +1,70 @@
<script setup lang="ts">
import IntlMessageFormat, { type FormatXMLElementFn, type PrimitiveType } from 'intl-messageformat'
import { computed, useSlots, type VNode } from 'vue'
import { useI18n } from 'vue-i18n'
import type { MessageDescriptor } from '../../composables/i18n'
const props = defineProps<{
messageId: MessageDescriptor
values?: Record<string, PrimitiveType>
}>()
const slots = useSlots()
const { t, locale } = useI18n()
const formattedParts = computed(() => {
const key = props.messageId.id
const translation = t(key, {}) as string
let msg: string
if (translation && translation !== key) {
msg = translation
} else {
msg = props.messageId.defaultMessage ?? key
}
const slotHandlers: Record<string, FormatXMLElementFn<VNode>> = {}
const slotNames = Object.keys(slots)
for (const slotName of slotNames) {
const normalizedName = slotName.startsWith('~') ? slotName.slice(1) : slotName
slotHandlers[normalizedName] = (chunks) => {
const slot = slots[slotName]
if (slot) {
return slot({
children: chunks,
})
}
return chunks as VNode[]
}
msg = msg.replace(
new RegExp(`\\{${normalizedName}\\}`, 'g'),
`<${normalizedName}></${normalizedName}>`,
)
}
try {
const formatter = new IntlMessageFormat(msg, locale.value)
const result = formatter.format({
...props.values,
...slotHandlers,
})
if (Array.isArray(result)) {
return result
}
return [result]
} catch {
return [msg]
}
})
</script>
<template>
<template v-for="(part, index) in formattedParts" :key="index">
<component :is="() => part" v-if="typeof part === 'object'" />
<template v-else>{{ part }}</template>
</template>
</template>

View File

@@ -292,11 +292,11 @@ import {
XIcon,
YouTubeIcon,
} from '@modrinth/assets'
import NewModal from '@modrinth/ui/src/components/modal/NewModal.vue'
import { markdownCommands, modrinthMarkdownEditorKeymap } from '@modrinth/utils/codemirror'
import { renderHighlightedString } from '@modrinth/utils/highlightjs'
import { type Component, computed, onBeforeUnmount, onMounted, ref, toRef, watch } from 'vue'
import NewModal from '../modal/NewModal.vue'
import Button from './Button.vue'
import Chips from './Chips.vue'
import FileInput from './FileInput.vue'

View File

@@ -31,7 +31,7 @@
</template>
<script setup lang="ts" generic="T">
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
const modelValue = defineModel<T>({ required: true })

View File

@@ -34,9 +34,9 @@
<script setup lang="ts">
import { XIcon } from '@modrinth/assets'
import { renderString } from '@modrinth/utils'
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { computed } from 'vue'
import { defineMessages, type MessageDescriptor, useVIntl } from '../../composables/i18n'
import Admonition from './Admonition.vue'
import ButtonStyled from './ButtonStyled.vue'
import CopyCode from './CopyCode.vue'

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import type { MessageDescriptor } from '@vintl/vintl'
import { useVIntl } from '@vintl/vintl'
import { computed } from 'vue'
import type { MessageDescriptor } from '../../composables/i18n'
import { useVIntl } from '../../composables/i18n'
const { formatMessage } = useVIntl()
const props = withDefaults(

View File

@@ -1,8 +1,8 @@
<script setup lang="ts" generic="T">
import { HistoryIcon, SaveIcon, SpinnerIcon } from '@modrinth/assets'
import { defineMessage, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { type Component, computed } from 'vue'
import { defineMessage, type MessageDescriptor, useVIntl } from '../../composables/i18n'
import { commonMessages } from '../../utils'
import ButtonStyled from './ButtonStyled.vue'

View File

@@ -29,6 +29,7 @@ export { default as FilterBar } from './FilterBar.vue'
export { default as HeadingLink } from './HeadingLink.vue'
export { default as HorizontalRule } from './HorizontalRule.vue'
export { default as IconSelect } from './IconSelect.vue'
export { default as IntlFormatted } from './IntlFormatted.vue'
export type { JoinedButtonAction } from './JoinedButtons.vue'
export { default as JoinedButtons } from './JoinedButtons.vue'
export { default as LoadingIndicator } from './LoadingIndicator.vue'

View File

@@ -1,7 +1,8 @@
<script setup lang="ts">
import { PlusIcon, XIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { nextTick, ref, useTemplateRef } from 'vue'
import { defineMessages, useVIntl } from '../../composables/i18n'
import { commonMessages } from '../../utils'
import { ButtonStyled, NewModal } from '../index'
import type { AddPaymentMethodProps } from './AddPaymentMethod.vue'

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { SpinnerIcon } from '@modrinth/assets'
import { formatPrice } from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
import { computed } from 'vue'
import { useVIntl } from '../../composables/i18n'
import Accordion from '../base/Accordion.vue'
const { locale } = useVIntl()

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { useVIntl } from '@vintl/vintl'
import type Stripe from 'stripe'
import { useVIntl } from '../../composables/i18n'
import { commonMessages, getPaymentMethodIcon, paymentMethodMessages } from '../../utils'
const { formatMessage } = useVIntl()

View File

@@ -2,10 +2,10 @@
import type { Labrinth } from '@modrinth/api-client'
import { InfoIcon } from '@modrinth/assets'
import { formatPrice } from '@modrinth/utils'
import { type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { Menu } from 'floating-vue'
import { computed, inject, type Ref } from 'vue'
import { type MessageDescriptor, useVIntl } from '../../composables/i18n'
import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils'
import type { ServerBillingInterval } from './ModrinthServersPurchaseModal.vue'
import ServersSpecs from './ServersSpecs.vue'

View File

@@ -8,10 +8,10 @@ import {
SpinnerIcon,
XIcon,
} from '@modrinth/assets'
import { defineMessage, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import type Stripe from 'stripe'
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
import { defineMessage, type MessageDescriptor, useVIntl } from '../../composables/i18n'
import { useStripe } from '../../composables/stripe'
import { commonMessages } from '../../utils'
import { ButtonStyled } from '../index'

View File

@@ -542,11 +542,11 @@ import {
XIcon,
} from '@modrinth/assets'
import { calculateSavings, createStripeElements, formatPrice, getCurrency } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import dayjs from 'dayjs'
import { computed, nextTick, reactive, ref, watch } from 'vue'
import { Multiselect } from 'vue-multiselect'
import { defineMessages, useVIntl } from '../../composables/i18n'
import Admonition from '../base/Admonition.vue'
import Checkbox from '../base/Checkbox.vue'
import Slider from '../base/Slider.vue'

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { formatPrice } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { computed, provide } from 'vue'
import { defineMessages, useVIntl } from '../../composables/i18n'
import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils'
import OptionGroup from '../base/OptionGroup.vue'
import ModalBasedServerPlan from './ModalBasedServerPlan.vue'

View File

@@ -1,13 +1,13 @@
<script setup lang="ts">
import type { Archon, Labrinth } from '@modrinth/api-client'
import { InfoIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { computed, onMounted, ref, watch } from 'vue'
import { formatPrice } from '../../../../utils'
import { defineMessages, useVIntl } from '../../composables/i18n'
import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils.ts'
import { regionOverrides } from '../../utils/regions.ts'
import IntlFormatted from '../base/IntlFormatted.vue'
import Slider from '../base/Slider.vue'
import ModalLoadingIndicator from '../modal/ModalLoadingIndicator.vue'
import type { RegionPing, ServerBillingInterval } from './ModrinthServersPurchaseModal.vue'

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { defineMessages, useVIntl } from '@vintl/vintl'
import type Stripe from 'stripe'
import { defineMessages, useVIntl } from '../../composables/i18n'
import PaymentMethodOption from './PaymentMethodOption.vue'
const { formatMessage } = useVIntl()

View File

@@ -11,11 +11,11 @@ import {
XIcon,
} from '@modrinth/assets'
import { formatPrice, getPingLevel } from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
import dayjs from 'dayjs'
import type Stripe from 'stripe'
import { computed } from 'vue'
import { useVIntl } from '../../composables/i18n'
import { getPriceForInterval, monthsInInterval } from '../../utils/product-utils'
import { regionOverrides } from '../../utils/regions'
import ButtonStyled from '../base/ButtonStyled.vue'

View File

@@ -2,11 +2,10 @@
import type { Archon } from '@modrinth/api-client'
import { SignalIcon, SpinnerIcon } from '@modrinth/assets'
import { getPingLevel } from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
import { computed } from 'vue'
import { useVIntl } from '../../composables/i18n'
import { regionOverrides } from '../../utils/regions'
const { formatMessage } = useVIntl()
const currentRegion = defineModel<string | undefined>({ required: true })

View File

@@ -44,11 +44,11 @@
<script setup lang="ts">
import { renderHighlightedString } from '@modrinth/utils'
import type { VersionEntry } from '@modrinth/utils/changelog'
import { defineMessages, useVIntl } from '@vintl/vintl'
import dayjs from 'dayjs'
import { computed, ref } from 'vue'
import { useRelativeTime } from '../../composables'
import { defineMessages, useVIntl } from '../../composables/i18n'
import AutoLink from '../base/AutoLink.vue'
const { formatMessage } = useVIntl()

View File

@@ -1,7 +1,8 @@
<script setup lang="ts">
import { type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { type Component, ref } from 'vue'
import { type MessageDescriptor, useVIntl } from '../../composables/i18n'
const { formatMessage } = useVIntl()
export type Tab<Props> = {

View File

@@ -206,11 +206,11 @@ import {
type GameVersionTag,
type Version,
} from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
import { computed, type Ref, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useRelativeTime } from '../../composables'
import { useVIntl } from '../../composables/i18n'
import { commonMessages } from '../../utils/common-messages'
import AutoLink from '../base/AutoLink.vue'
import TagItem from '../base/TagItem.vue'

View File

@@ -87,10 +87,15 @@
import { ClientIcon, MonitorSmartphoneIcon, ServerIcon, UserIcon } from '@modrinth/assets'
import type { EnvironmentV3, GameVersionTag, PlatformTag, ProjectV3Partial } from '@modrinth/utils'
import { formatCategory, getVersionsToDisplay } from '@modrinth/utils'
import { defineMessage, defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { type Component, computed } from 'vue'
import { useRouter } from 'vue-router'
import {
defineMessage,
defineMessages,
type MessageDescriptor,
useVIntl,
} from '../../composables/i18n'
import TagItem from '../base/TagItem.vue'
const { formatMessage } = useVIntl()

View File

@@ -48,9 +48,9 @@
</template>
<script setup lang="ts">
import { CrownIcon, ExternalIcon, OrganizationIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { computed } from 'vue'
import { defineMessages, useVIntl } from '../../composables/i18n'
import AutoLink from '../base/AutoLink.vue'
import Avatar from '../base/Avatar.vue'

View File

@@ -65,11 +65,11 @@
</template>
<script setup lang="ts">
import { BookTextIcon, CalendarIcon, ExternalIcon, ScaleIcon, VersionIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '@vintl/vintl'
import dayjs from 'dayjs'
import { computed } from 'vue'
import { useRelativeTime } from '../../composables'
import { defineMessages, useVIntl } from '../../composables/i18n'
const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime()

View File

@@ -102,7 +102,8 @@ import {
PayPalIcon,
WikiIcon,
} from '@modrinth/assets'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { defineMessages, useVIntl } from '../../composables/i18n'
const { formatMessage } = useVIntl()

View File

@@ -4,9 +4,9 @@
<script setup lang="ts">
import type { ProjectStatus } from '@modrinth/utils'
import { defineMessage, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { computed } from 'vue'
import { defineMessage, type MessageDescriptor, useVIntl } from '../../composables/i18n'
import { PROJECT_STATUS_ICONS } from '../../utils'
import Badge from '../base/SimpleBadge.vue'

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { defineMessage, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { computed, ref, watch } from 'vue'
import { defineMessage, type MessageDescriptor, useVIntl } from '../../../../composables/i18n'
import { commonProjectSettingsMessages } from '../../../../utils'
import LargeRadioButton from '../../../base/LargeRadioButton.vue'

View File

@@ -33,9 +33,9 @@
<script setup lang="ts">
import { BanIcon, LockIcon, XCircleIcon, XIcon } from '@modrinth/assets'
import { defineMessage, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { computed, type ComputedRef } from 'vue'
import { defineMessage, type MessageDescriptor, useVIntl } from '../../composables/i18n'
import type { FilterOption, FilterType, FilterValue } from '../../utils/search'
import TagItem from '../base/TagItem.vue'

View File

@@ -157,9 +157,9 @@ import {
UpdatedIcon,
XIcon,
} from '@modrinth/assets'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { computed, ref } from 'vue'
import { defineMessages, useVIntl } from '../../composables/i18n'
import type { FilterOption, FilterType, FilterValue } from '../../utils/search'
import Accordion from '../base/Accordion.vue'
import ButtonStyled from '../base/ButtonStyled.vue'

View File

@@ -13,10 +13,10 @@ import {
UserRoundIcon,
XIcon,
} from '@modrinth/assets'
import { defineMessages, useVIntl } from '@vintl/vintl'
import dayjs from 'dayjs'
import { computed } from 'vue'
import { defineMessages, useVIntl } from '../../../composables/i18n'
import { commonMessages } from '../../../utils'
import ButtonStyled from '../../base/ButtonStyled.vue'
import OverflowMenu, { type Option as OverflowOption } from '../../base/OverflowMenu.vue'

View File

@@ -1,7 +1,7 @@
<script setup lang="ts" generic="T extends string">
import { MoonIcon, RadioButtonCheckedIcon, RadioButtonIcon, SunIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { defineMessages, useVIntl } from '../../composables/i18n'
const { formatMessage } = useVIntl()
const { updateColorTheme, currentTheme, themeOptions, systemThemeColor } = defineProps<{

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { VersionChannel } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { defineMessages, useVIntl } from '../../composables/i18n'
const { formatMessage } = useVIntl()

View File

@@ -31,6 +31,7 @@
<script setup lang="ts">
import { DownloadIcon, ExternalIcon } from '@modrinth/assets'
import type { Version, VersionFile } from '@modrinth/utils'
import { computed } from 'vue'
import { ButtonStyled, VersionChannelIndicator } from '../index'