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

@@ -0,0 +1,316 @@
<script setup lang="ts">
import { BlueskyIcon, DiscordIcon, GithubIcon, MastodonIcon, TwitterIcon } from '@modrinth/assets'
import {
ButtonStyled,
defineMessage,
defineMessages,
injectNotificationManager,
IntlFormatted,
type MessageDescriptor,
useVIntl,
} from '@modrinth/ui'
import TextLogo from '~/components/brand/TextLogo.vue'
const flags = useFeatureFlags()
const { formatMessage } = useVIntl()
const { addNotification } = injectNotificationManager()
const messages = defineMessages({
modrinthInformation: {
id: 'layout.footer.modrinth-information',
defaultMessage: 'Modrinth information',
},
openSource: {
id: 'layout.footer.open-source',
defaultMessage: 'Modrinth is <github-link>open source</github-link>.',
},
legalDisclaimer: {
id: 'layout.footer.legal-disclaimer',
defaultMessage:
'NOT AN OFFICIAL MINECRAFT SERVICE. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT.',
},
})
const socialLinks: {
label: MessageDescriptor
href: string
icon: Component
rel?: string
}[] = [
{
label: defineMessage({ id: 'layout.footer.social.discord', defaultMessage: 'Discord' }),
href: 'https://discord.modrinth.com',
icon: DiscordIcon,
},
{
label: defineMessage({ id: 'layout.footer.social.bluesky', defaultMessage: 'Bluesky' }),
href: 'https://bsky.app/profile/modrinth.com',
icon: BlueskyIcon,
},
{
label: defineMessage({ id: 'layout.footer.social.mastodon', defaultMessage: 'Mastodon' }),
href: 'https://floss.social/@modrinth',
icon: MastodonIcon,
rel: 'me',
},
{
label: defineMessage({ id: 'layout.footer.social.x', defaultMessage: 'X' }),
href: 'https://x.com/modrinth',
icon: TwitterIcon,
},
{
label: defineMessage({ id: 'layout.footer.social.github', defaultMessage: 'GitHub' }),
href: 'https://github.com/modrinth',
icon: GithubIcon,
},
]
const footerLinks: {
label: MessageDescriptor
links: {
href: string
label: MessageDescriptor
}[]
}[] = [
{
label: defineMessage({ id: 'layout.footer.about', defaultMessage: 'About' }),
links: [
{
href: '/news',
label: defineMessage({ id: 'layout.footer.about.news', defaultMessage: 'News' }),
},
{
href: '/news/changelog',
label: defineMessage({ id: 'layout.footer.about.changelog', defaultMessage: 'Changelog' }),
},
{
href: 'https://status.modrinth.com',
label: defineMessage({ id: 'layout.footer.about.status', defaultMessage: 'Status' }),
},
{
href: 'https://careers.modrinth.com',
label: defineMessage({ id: 'layout.footer.about.careers', defaultMessage: 'Careers' }),
},
{
href: '/legal/cmp-info',
label: defineMessage({
id: 'layout.footer.about.rewards-program',
defaultMessage: 'Rewards Program',
}),
},
],
},
{
label: defineMessage({ id: 'layout.footer.products', defaultMessage: 'Products' }),
links: [
{
href: '/plus',
label: defineMessage({ id: 'layout.footer.products.plus', defaultMessage: 'Modrinth+' }),
},
{
href: '/app',
label: defineMessage({ id: 'layout.footer.products.app', defaultMessage: 'Modrinth App' }),
},
{
href: '/hosting',
label: defineMessage({
id: 'layout.footer.products.servers',
defaultMessage: 'Modrinth Hosting',
}),
},
],
},
{
label: defineMessage({ id: 'layout.footer.resources', defaultMessage: 'Resources' }),
links: [
{
href: 'https://support.modrinth.com',
label: defineMessage({
id: 'layout.footer.resources.help-center',
defaultMessage: 'Help Center',
}),
},
{
href: 'https://translate.modrinth.com',
label: defineMessage({
id: 'layout.footer.resources.translate',
defaultMessage: 'Translate',
}),
},
{
href: 'https://github.com/modrinth/code/issues',
label: defineMessage({
id: 'layout.footer.resources.report-issues',
defaultMessage: 'Report issues',
}),
},
{
href: 'https://docs.modrinth.com/api/',
label: defineMessage({
id: 'layout.footer.resources.api-docs',
defaultMessage: 'API documentation',
}),
},
],
},
{
label: defineMessage({ id: 'layout.footer.legal', defaultMessage: 'Legal' }),
links: [
{
href: '/legal/rules',
label: defineMessage({ id: 'layout.footer.legal.rules', defaultMessage: 'Content Rules' }),
},
{
href: '/legal/terms',
label: defineMessage({
id: 'layout.footer.legal.terms-of-use',
defaultMessage: 'Terms of Use',
}),
},
{
href: '/legal/privacy',
label: defineMessage({
id: 'layout.footer.legal.privacy-policy',
defaultMessage: 'Privacy Policy',
}),
},
{
href: '/legal/security',
label: defineMessage({
id: 'layout.footer.legal.security-notice',
defaultMessage: 'Security Notice',
}),
},
{
href: '/legal/copyright',
label: defineMessage({
id: 'layout.footer.legal.copyright-policy',
defaultMessage: 'Copyright Policy and DMCA',
}),
},
],
},
]
const developerModeCounter = ref(0)
const state = useGeneratedState()
function developerModeIncrement() {
if (developerModeCounter.value >= 5) {
flags.value.developerMode = !flags.value.developerMode
developerModeCounter.value = 0
saveFeatureFlags()
if (flags.value.developerMode) {
addNotification({
title: 'Developer mode activated',
text: 'Developer mode has been enabled',
type: 'success',
})
} else {
addNotification({
title: 'Developer mode deactivated',
text: 'Developer mode has been disabled',
type: 'success',
})
}
} else {
developerModeCounter.value++
}
}
</script>
<template>
<footer
class="footer-brand-background experimental-styles-within border-0 border-t-[1px] border-solid"
>
<div class="mx-auto flex max-w-screen-xl flex-col gap-6 p-6 pb-20 sm:px-12 md:py-12">
<div
class="grid grid-cols-1 gap-4 text-primary md:grid-cols-[1fr_2fr] lg:grid-cols-[auto_auto_auto_auto_auto]"
>
<div
class="flex flex-col items-center gap-3 md:items-start"
role="region"
:aria-label="formatMessage(messages.modrinthInformation)"
>
<TextLogo
aria-hidden="true"
class="text-logo button-base h-6 w-auto text-contrast lg:h-8"
@click="developerModeIncrement()"
/>
<div class="flex flex-wrap justify-center gap-px sm:-mx-2">
<ButtonStyled
v-for="(social, index) in socialLinks"
:key="`footer-social-${index}`"
circular
type="transparent"
>
<a
v-tooltip="formatMessage(social.label)"
:href="social.href"
target="_blank"
:rel="`noopener${social.rel ? ` ${social.rel}` : ''}`"
>
<component :is="social.icon" class="h-5 w-5" />
</a>
</ButtonStyled>
</div>
<div class="mt-auto flex flex-wrap justify-center gap-3 md:flex-col">
<p class="m-0">
<IntlFormatted :message-id="messages.openSource">
<template #github-link="{ children }">
<a
href="https://github.com/modrinth/code"
class="text-brand hover:underline"
target="_blank"
rel="noopener"
>
<component :is="() => children" />
</a>
</template>
</IntlFormatted>
</p>
<p class="m-0">© {{ state.buildYear ?? '2025' }} Rinth, Inc.</p>
</div>
</div>
<div class="mt-4 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:contents">
<div
v-for="group in footerLinks"
:key="group.label.id"
class="flex flex-col items-center gap-3 sm:items-start"
>
<h3 class="m-0 text-base text-contrast">{{ formatMessage(group.label) }}</h3>
<template v-for="item in group.links" :key="item.label">
<nuxt-link
v-if="item.href.startsWith('/')"
:to="item.href"
class="w-fit hover:underline"
>
{{ formatMessage(item.label) }}
</nuxt-link>
<a
v-else
:href="item.href"
class="w-fit hover:underline"
target="_blank"
rel="noopener"
>
{{ formatMessage(item.label) }}
</a>
</template>
</div>
</div>
</div>
<div class="flex justify-center text-center text-xs font-medium text-secondary opacity-50">
{{ formatMessage(messages.legalDisclaimer) }}
</div>
</div>
</footer>
</template>
<style scoped lang="scss">
.footer-brand-background {
background: var(--brand-gradient-strong-bg);
border-color: var(--brand-gradient-border);
}
</style>

View File

@@ -24,9 +24,14 @@
<script setup lang="ts">
import { CheckIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager } from '@modrinth/ui'
import {
ButtonStyled,
defineMessages,
injectNotificationManager,
type MessageDescriptor,
useVIntl,
} from '@modrinth/ui'
import type { Project, User, Version } from '@modrinth/utils'
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { computed } from 'vue'
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'

View File

@@ -87,7 +87,13 @@
<script lang="ts" setup>
import type { Labrinth } from '@modrinth/api-client'
import { Admonition, DropzoneFileInput, injectProjectPageContext } from '@modrinth/ui'
import {
Admonition,
defineMessages,
DropzoneFileInput,
injectProjectPageContext,
useVIntl,
} from '@modrinth/ui'
import { acceptFileFromProjectType } from '@modrinth/utils'
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'

View File

@@ -58,8 +58,13 @@
</template>
<script setup>
import { PlusIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, NewModal } from '@modrinth/ui'
import { defineMessages } from '@vintl/vintl'
import {
ButtonStyled,
defineMessages,
injectNotificationManager,
NewModal,
useVIntl,
} from '@modrinth/ui'
import CreateLimitAlert from './CreateLimitAlert.vue'

View File

@@ -42,9 +42,8 @@
<script setup lang="ts">
import { MessageIcon } from '@modrinth/assets'
import { Admonition, ButtonStyled } from '@modrinth/ui'
import { Admonition, ButtonStyled, defineMessages, useVIntl } from '@modrinth/ui'
import { capitalizeString } from '@modrinth/utils'
import { defineMessages } from '@vintl/vintl'
import { computed, watch } from 'vue'
const { formatMessage } = useVIntl()

View File

@@ -81,8 +81,13 @@
<script setup lang="ts">
import { PlusIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, NewModal } from '@modrinth/ui'
import { defineMessages } from '@vintl/vintl'
import {
ButtonStyled,
defineMessages,
injectNotificationManager,
NewModal,
useVIntl,
} from '@modrinth/ui'
import { ref } from 'vue'
import CreateLimitAlert from './CreateLimitAlert.vue'

View File

@@ -95,8 +95,14 @@
<script setup>
import { PlusIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, Chips, injectNotificationManager, NewModal } from '@modrinth/ui'
import { defineMessages } from '@vintl/vintl'
import {
ButtonStyled,
Chips,
defineMessages,
injectNotificationManager,
NewModal,
useVIntl,
} from '@modrinth/ui'
import CreateLimitAlert from './CreateLimitAlert.vue'

View File

@@ -162,12 +162,13 @@ import {
Admonition,
ButtonStyled,
Chips,
defineMessages,
injectNotificationManager,
IntlFormatted,
NewModal,
normalizeChildren,
useVIntl,
} from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { type FormRequestResponse, useAvalara1099 } from '@/composables/avalara1099'

View File

@@ -116,8 +116,14 @@ import {
SpinnerIcon,
XIcon,
} from '@modrinth/assets'
import { ButtonStyled, commonMessages, injectNotificationManager, NewModal } from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import {
ButtonStyled,
commonMessages,
defineMessages,
injectNotificationManager,
NewModal,
useVIntl,
} from '@modrinth/ui'
import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue'
import {

View File

@@ -49,9 +49,14 @@
</template>
<script setup lang="ts">
import { ButtonStyled, Combobox, commonMessages, formFieldPlaceholders } from '@modrinth/ui'
import {
ButtonStyled,
Combobox,
commonMessages,
formFieldPlaceholders,
useVIntl,
} from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
import { computed, nextTick, ref, watch } from 'vue'
const props = withDefaults(

View File

@@ -57,8 +57,8 @@
<script setup lang="ts">
import { LoaderCircleIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { computed } from 'vue'
const props = withDefaults(

View File

@@ -124,10 +124,8 @@
</template>
<script setup lang="ts">
import { normalizeChildren } from '@modrinth/ui'
import { defineMessages, IntlFormatted, normalizeChildren, useVIntl } from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import dayjs from 'dayjs'
import { computed, onMounted, ref } from 'vue'
import ConfettiExplosion from 'vue-confetti-explosion'

View File

@@ -107,12 +107,13 @@ import { CheckIcon, PayPalColorIcon, SaveIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
Checkbox,
defineMessages,
financialMessages,
formFieldLabels,
IntlFormatted,
normalizeChildren,
useVIntl,
} from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { useDebounceFn } from '@vueuse/core'
import { computed, onMounted, ref, watch } from 'vue'

View File

@@ -83,13 +83,14 @@ import {
Admonition,
ButtonStyled,
Combobox,
defineMessages,
injectNotificationManager,
IntlFormatted,
normalizeChildren,
useDebugLogger,
useVIntl,
} from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { useGeolocation } from '@vueuse/core'
import { useCountries, useFormattedCountries, useUserCountry } from '@/composables/country.ts'

View File

@@ -204,6 +204,7 @@ import {
Admonition,
Checkbox,
Combobox,
defineMessages,
financialMessages,
formFieldLabels,
formFieldPlaceholders,
@@ -211,10 +212,10 @@ import {
getBlockchainIcon,
getCurrencyColor,
getCurrencyIcon,
IntlFormatted,
normalizeChildren,
useVIntl,
} from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { useDebounceFn } from '@vueuse/core'
import { computed, ref, watch } from 'vue'

View File

@@ -218,8 +218,14 @@
</template>
<script setup lang="ts">
import { Chips, Combobox, formFieldLabels, formFieldPlaceholders } from '@modrinth/ui'
import { useVIntl } from '@vintl/vintl'
import {
Chips,
Combobox,
defineMessages,
formFieldLabels,
formFieldPlaceholders,
useVIntl,
} from '@modrinth/ui'
// TODO: Switch to using Muralpay's improved endpoint when it's available.
import iso3166 from 'iso-3166-2'

View File

@@ -74,10 +74,15 @@
<script setup lang="ts">
import { FileTextIcon } from '@modrinth/assets'
import { Admonition, ButtonStyled, normalizeChildren } from '@modrinth/ui'
import {
Admonition,
ButtonStyled,
defineMessages,
IntlFormatted,
normalizeChildren,
useVIntl,
} from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { computed } from 'vue'
import { TAX_THRESHOLD_ACTUAL } from '@/providers/creator-withdraw.ts'

View File

@@ -356,16 +356,17 @@ import {
Checkbox,
Chips,
Combobox,
defineMessages,
financialMessages,
formFieldLabels,
formFieldPlaceholders,
IntlFormatted,
normalizeChildren,
paymentMethodMessages,
useDebugLogger,
useVIntl,
} from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { useDebounceFn } from '@vueuse/core'
import { computed, onMounted, ref, watch } from 'vue'

View File

@@ -89,9 +89,8 @@ import {
} from '@modrinth/assets'
import type { Nag, NagContext, NagStatus } from '@modrinth/moderation'
import { nags } from '@modrinth/moderation'
import { ButtonStyled } from '@modrinth/ui'
import { ButtonStyled, defineMessages, type MessageDescriptor, useVIntl } from '@modrinth/ui'
import type { Project, User, Version } from '@modrinth/utils'
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import type { Component } from 'vue'
import { computed } from 'vue'

View File

@@ -29,8 +29,7 @@
<script setup lang="ts">
import { NewspaperIcon } from '@modrinth/assets'
import { articles as rawArticles } from '@modrinth/blog'
import { ButtonStyled, NewsArticleCard } from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { ButtonStyled, defineMessages, NewsArticleCard, useVIntl } from '@modrinth/ui'
import { computed, ref } from 'vue'
const { formatMessage } = useVIntl()

View File

@@ -120,7 +120,7 @@ import {
UpdatedIcon,
XIcon,
} from '@modrinth/assets'
import { ButtonStyled, Checkbox, NewModal, ServerInfoLabels } from '@modrinth/ui'
import { ButtonStyled, Checkbox, NewModal, ServerInfoLabels, useVIntl } from '@modrinth/ui'
import type { PowerAction as ServerPowerAction, ServerState } from '@modrinth/utils'
import { useStorage } from '@vueuse/core'
import { computed, ref } from 'vue'

View File

@@ -213,6 +213,7 @@ import {
injectNotificationManager,
NewModal,
Toggle,
useVIntl,
} from '@modrinth/ui'
import { type Loaders, ModrinthServersFetchError } from '@modrinth/utils'
import { $fetch } from 'ofetch'

View File

@@ -159,7 +159,7 @@
<script setup lang="ts">
import { CompassIcon, InfoIcon, SettingsIcon, TransferIcon, UploadIcon } from '@modrinth/assets'
import { ButtonStyled, NewProjectCard } from '@modrinth/ui'
import { ButtonStyled, NewProjectCard, useVIntl } from '@modrinth/ui'
import type { Loaders } from '@modrinth/utils'
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ButtonStyled, ServersSpecs } from '@modrinth/ui'
import type { MessageDescriptor } from '@modrinth/ui'
import { ButtonStyled, defineMessage, ServersSpecs, useVIntl } from '@modrinth/ui'
import { formatPrice } from '@modrinth/utils'
import type { MessageDescriptor } from '@vintl/vintl'
const { formatMessage, locale } = useVIntl()

View File

@@ -7,9 +7,9 @@ import {
ServerNotice,
TagItem,
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import type { ServerNotice as ServerNoticeType } from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
import dayjs from 'dayjs'
const { formatMessage } = useVIntl()