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()

View File

@@ -35,6 +35,7 @@ export interface GeneratedState extends Labrinth.State.GeneratedState {
// Metadata
lastGenerated?: string
apiUrl?: string
buildYear: number
}
/**
@@ -121,4 +122,6 @@ export const useGeneratedState = () =>
lastGenerated: generatedState.lastGenerated,
apiUrl: generatedState.apiUrl,
errors: generatedState.errors,
buildYear: new Date().getFullYear(),
}))

View File

@@ -53,13 +53,14 @@
<script setup>
import { SadRinthbot } from '@modrinth/assets'
import {
defineMessage,
IntlFormatted,
NotificationPanel,
provideModrinthClient,
provideNotificationManager,
providePageContext,
useVIntl,
} from '@modrinth/ui'
import { defineMessage, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import Logo404 from '~/assets/images/404.svg'

View File

@@ -0,0 +1,15 @@
import { buildLocaleMessages, createMessageCompiler, type CrowdinMessages } from '@modrinth/ui'
const localeModules = import.meta.glob<{ default: CrowdinMessages }>('./locales/*/index.json', {
eager: true,
})
export default defineI18nConfig(() => ({
legacy: false,
locale: 'en-US',
fallbackLocale: 'en-US',
messageCompiler: createMessageCompiler(),
missingWarn: false,
fallbackWarn: false,
messages: buildLocaleMessages(localeModules),
}))

View File

@@ -821,93 +821,7 @@
<BatchCreditModal v-if="auth.user && isAdmin(auth.user)" ref="modal_batch_credit" />
<slot id="main" />
</main>
<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="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="footerMessages.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">
{{ formatMessage(footerMessages.copyright, { year: currentYear }) }}
</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"
class="flex flex-col items-center gap-3 sm:items-start"
>
<h3 class="m-0 text-base text-contrast">{{ 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"
>
{{ item.label }}
</nuxt-link>
<a
v-else
:href="item.href"
class="w-fit hover:underline"
target="_blank"
rel="noopener"
>
{{ item.label }}
</a>
</template>
</div>
</div>
</div>
<div class="flex justify-center text-center text-xs font-medium text-secondary opacity-50">
{{ formatMessage(footerMessages.legalDisclaimer) }}
</div>
</div>
</footer>
<ModrinthFooter />
</div>
</template>
<script setup>
@@ -915,7 +829,6 @@ import {
AffiliateIcon,
ArrowBigUpDashIcon,
BellIcon,
BlueskyIcon,
BookTextIcon,
BoxIcon,
BracesIcon,
@@ -923,12 +836,10 @@ import {
CollectionIcon,
CompassIcon,
CurrencyIcon,
DiscordIcon,
DownloadIcon,
DropdownIcon,
FileIcon,
FileTextIcon,
GithubIcon,
GlassesIcon,
HamburgerIcon,
HomeIcon,
@@ -936,7 +847,6 @@ import {
LibraryIcon,
LogInIcon,
LogOutIcon,
MastodonIcon,
MessageIcon,
ModrinthIcon,
MoonIcon,
@@ -952,7 +862,6 @@ import {
SettingsIcon,
ShieldAlertIcon,
SunIcon,
TwitterIcon,
UserIcon,
UserSearchIcon,
XIcon,
@@ -963,12 +872,13 @@ import {
ButtonStyled,
commonMessages,
commonProjectTypeCategoryMessages,
defineMessages,
injectNotificationManager,
OverflowMenu,
PagewideBanner,
useVIntl,
} from '@modrinth/ui'
import { isAdmin, isStaff, UserBadge } from '@modrinth/utils'
import { IntlFormatted } from '@vintl/vintl/components'
import TextLogo from '~/components/brand/TextLogo.vue'
import BatchCreditModal from '~/components/ui/admin/BatchCreditModal.vue'
@@ -976,6 +886,7 @@ import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.
import OrganizationCreateModal from '~/components/ui/create/OrganizationCreateModal.vue'
import ProjectCreateModal from '~/components/ui/create/ProjectCreateModal.vue'
import CreatorTaxFormModal from '~/components/ui/dashboard/CreatorTaxFormModal.vue'
import ModrinthFooter from '~/components/ui/ModrinthFooter.vue'
import TeleportOverflowMenu from '~/components/ui/servers/TeleportOverflowMenu.vue'
import { errors as generatedStateErrors } from '~/generated/state.json'
import { getProjectTypeMessage } from '~/utils/i18n-project-type.ts'
@@ -1196,10 +1107,6 @@ const messages = defineMessages({
id: 'layout.nav.modrinth-home-page',
defaultMessage: 'Modrinth home page',
},
modrinthInformation: {
id: 'layout.footer.modrinth-information',
defaultMessage: 'Modrinth information',
},
createNew: {
id: 'layout.action.create-new',
defaultMessage: 'Create new...',
@@ -1294,22 +1201,6 @@ const messages = defineMessages({
},
})
const footerMessages = defineMessages({
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.',
},
copyright: {
id: 'layout.footer.copyright',
defaultMessage: '© {year} Rinth, Inc.',
},
})
useHead({
link: [
{
@@ -1348,10 +1239,6 @@ useSeoMeta({
twitterSite: '@modrinth',
})
const developerModeCounter = ref(0)
const currentYear = new Date().getFullYear()
const isMobileMenuOpen = ref(false)
const isBrowseMenuOpen = ref(false)
const navRoutes = computed(() => [
@@ -1547,29 +1434,6 @@ watch(
},
)
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++
}
}
async function logoutUser() {
await logout()
}
@@ -1620,196 +1484,6 @@ function hideRussiaCensorshipBanner() {
flags.value.hideRussiaCensorshipBanner = true
saveFeatureFlags()
}
const socialLinks = [
{
label: formatMessage(
defineMessage({ id: 'layout.footer.social.discord', defaultMessage: 'Discord' }),
),
href: 'https://discord.modrinth.com',
icon: DiscordIcon,
},
{
label: formatMessage(
defineMessage({ id: 'layout.footer.social.bluesky', defaultMessage: 'Bluesky' }),
),
href: 'https://bsky.app/profile/modrinth.com',
icon: BlueskyIcon,
},
{
label: formatMessage(
defineMessage({ id: 'layout.footer.social.mastodon', defaultMessage: 'Mastodon' }),
),
href: 'https://floss.social/@modrinth',
icon: MastodonIcon,
rel: 'me',
},
{
label: formatMessage(defineMessage({ id: 'layout.footer.social.x', defaultMessage: 'X' })),
href: 'https://x.com/modrinth',
icon: TwitterIcon,
},
{
label: formatMessage(
defineMessage({ id: 'layout.footer.social.github', defaultMessage: 'GitHub' }),
),
href: 'https://github.com/modrinth',
icon: GithubIcon,
},
]
const footerLinks = [
{
label: formatMessage(defineMessage({ id: 'layout.footer.about', defaultMessage: 'About' })),
links: [
{
href: '/news',
label: formatMessage(
defineMessage({ id: 'layout.footer.about.news', defaultMessage: 'News' }),
),
},
{
href: '/news/changelog',
label: formatMessage(
defineMessage({ id: 'layout.footer.about.changelog', defaultMessage: 'Changelog' }),
),
},
{
href: 'https://status.modrinth.com',
label: formatMessage(
defineMessage({ id: 'layout.footer.about.status', defaultMessage: 'Status' }),
),
},
{
href: 'https://careers.modrinth.com',
label: formatMessage(
defineMessage({ id: 'layout.footer.about.careers', defaultMessage: 'Careers' }),
),
},
{
href: '/legal/cmp-info',
label: formatMessage(
defineMessage({
id: 'layout.footer.about.rewards-program',
defaultMessage: 'Rewards Program',
}),
),
},
],
},
{
label: formatMessage(
defineMessage({ id: 'layout.footer.products', defaultMessage: 'Products' }),
),
links: [
{
href: '/plus',
label: formatMessage(
defineMessage({ id: 'layout.footer.products.plus', defaultMessage: 'Modrinth+' }),
),
},
{
href: '/app',
label: formatMessage(
defineMessage({ id: 'layout.footer.products.app', defaultMessage: 'Modrinth App' }),
),
},
{
href: '/hosting',
label: formatMessage(
defineMessage({
id: 'layout.footer.products.servers',
defaultMessage: 'Modrinth Hosting',
}),
),
},
],
},
{
label: formatMessage(
defineMessage({ id: 'layout.footer.resources', defaultMessage: 'Resources' }),
),
links: [
{
href: 'https://support.modrinth.com',
label: formatMessage(
defineMessage({
id: 'layout.footer.resources.help-center',
defaultMessage: 'Help Center',
}),
),
},
{
href: 'https://translate.modrinth.com',
label: formatMessage(
defineMessage({ id: 'layout.footer.resources.translate', defaultMessage: 'Translate' }),
),
},
{
href: 'https://github.com/modrinth/code/issues',
label: formatMessage(
defineMessage({
id: 'layout.footer.resources.report-issues',
defaultMessage: 'Report issues',
}),
),
},
{
href: 'https://docs.modrinth.com/api/',
label: formatMessage(
defineMessage({
id: 'layout.footer.resources.api-docs',
defaultMessage: 'API documentation',
}),
),
},
],
},
{
label: formatMessage(defineMessage({ id: 'layout.footer.legal', defaultMessage: 'Legal' })),
links: [
{
href: '/legal/rules',
label: formatMessage(
defineMessage({ id: 'layout.footer.legal.rules', defaultMessage: 'Content Rules' }),
),
},
{
href: '/legal/terms',
label: formatMessage(
defineMessage({ id: 'layout.footer.legal.terms-of-use', defaultMessage: 'Terms of Use' }),
),
},
{
href: '/legal/privacy',
label: formatMessage(
defineMessage({
id: 'layout.footer.legal.privacy-policy',
defaultMessage: 'Privacy Policy',
}),
),
},
{
href: '/legal/security',
label: formatMessage(
defineMessage({
id: 'layout.footer.legal.security-notice',
defaultMessage: 'Security Notice',
}),
),
},
{
href: '/legal/copyright',
label: formatMessage(
defineMessage({
id: 'layout.footer.legal.copyright-policy',
defaultMessage: 'Copyright Policy and DMCA',
}),
),
},
],
},
]
</script>
<style lang="scss">
@@ -2050,11 +1724,6 @@ const footerLinks = [
}
}
.footer-brand-background {
background: var(--brand-gradient-strong-bg);
border-color: var(--brand-gradient-border);
}
.over-the-top-random-animation {
position: fixed;
z-index: 100;

View File

@@ -1364,9 +1364,6 @@
"layout.footer.about.status": {
"message": "Status"
},
"layout.footer.copyright": {
"message": "© {year} Rinth, Inc."
},
"layout.footer.legal": {
"message": "Legal"
},
@@ -2813,18 +2810,9 @@
"settings.display.theme.title": {
"message": "Color theme"
},
"settings.language.categories.auto": {
"message": "Automatic"
},
"settings.language.categories.default": {
"message": "Standard languages"
},
"settings.language.categories.experimental": {
"message": "Experimental languages"
},
"settings.language.categories.fun": {
"message": "Fun languages"
},
"settings.language.categories.search-result": {
"message": "Search results"
},
@@ -2834,18 +2822,6 @@
"settings.language.languages.automatic": {
"message": "Sync with the system language"
},
"settings.language.languages.language-label-applying": {
"message": "{label}. Applying..."
},
"settings.language.languages.language-label-error": {
"message": "{label}. Error"
},
"settings.language.languages.load-failed": {
"message": "Cannot load this language. Try again in a bit."
},
"settings.language.languages.search-field.description": {
"message": "Submit to focus the first search result"
},
"settings.language.languages.search-field.placeholder": {
"message": "Search for a language..."
},

View File

@@ -970,7 +970,9 @@ import {
ButtonStyled,
Checkbox,
commonMessages,
defineMessages,
injectNotificationManager,
IntlFormatted,
NewModal,
OverflowMenu,
PopoutMenu,
@@ -985,10 +987,10 @@ import {
ServersPromo,
TagItem,
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import VersionSummary from '@modrinth/ui/src/components/version/VersionSummary.vue'
import { formatCategory, formatPrice, formatProjectType, renderString } from '@modrinth/utils'
import { IntlFormatted } from '@vintl/vintl/components'
import { useLocalStorage } from '@vueuse/core'
import dayjs from 'dayjs'
import { Tooltip } from 'floating-vue'

View File

@@ -14,9 +14,9 @@ import {
commonMessages,
commonProjectSettingsMessages,
injectNotificationManager,
useVIntl,
} from '@modrinth/ui'
import type { Project, ProjectV3Partial } from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
import { useLocalStorage, useScroll } from '@vueuse/core'
import { computed } from 'vue'

View File

@@ -3,14 +3,15 @@ import { CheckIcon } from '@modrinth/assets'
import {
Admonition,
commonProjectSettingsMessages,
defineMessages,
injectModrinthClient,
injectNotificationManager,
injectProjectPageContext,
ProjectSettingsEnvSelector,
UnsavedChangesPopup,
useSavable,
useVIntl,
} from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
const { formatMessage } = useVIntl()

View File

@@ -1,14 +1,16 @@
<script setup lang="ts">
import {
defineMessages,
IconSelect,
injectModrinthClient,
injectNotificationManager,
injectProjectPageContext,
type MessageDescriptor,
SettingsLabel,
UnsavedChangesPopup,
useSavable,
useVIntl,
} from '@modrinth/ui'
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
const { formatMessage } = useVIntl()

View File

@@ -326,11 +326,13 @@ import {
Avatar,
ButtonStyled,
CopyCode,
defineMessages,
DropdownSelect,
injectNotificationManager,
NewModal,
Toggle,
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import { formatCategory, formatPrice } from '@modrinth/utils'
import { DEFAULT_CREDIT_EMAIL_MESSAGE } from '@modrinth/utils/utils.ts'

View File

@@ -267,16 +267,17 @@ import {
Combobox,
commonMessages,
CopyCode,
defineMessages,
injectNotificationManager,
NewModal,
ServerNotice,
TagItem,
Toggle,
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import { NOTICE_LEVELS } from '@modrinth/ui/src/utils/notices.ts'
import type { ServerNotice as ServerNoticeType } from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
import dayjs from 'dayjs'
import { computed } from 'vue'

View File

@@ -8,9 +8,15 @@ import {
SendIcon,
TrashIcon,
} from '@modrinth/assets'
import { Avatar, Badge, Checkbox, commonMessages } from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import {
Avatar,
Badge,
Checkbox,
commonMessages,
defineMessages,
IntlFormatted,
useVIntl,
} from '@modrinth/ui'
import ATLauncher from '~/assets/images/external/atlauncher.svg?component'
import CurseForge from '~/assets/images/external/curseforge.svg?component'

View File

@@ -85,10 +85,12 @@ import {
Avatar,
Button,
commonMessages,
defineMessages,
injectNotificationManager,
IntlFormatted,
normalizeChildren,
useVIntl,
} from '@modrinth/ui'
import { IntlFormatted } from '@vintl/vintl/components'
import { useAuth } from '@/composables/auth.js'
import { useScopes } from '@/composables/auth/scopes.ts'

View File

@@ -68,7 +68,7 @@
</template>
<script setup>
import { KeyIcon, MailIcon, SendIcon } from '@modrinth/assets'
import { commonMessages, injectNotificationManager } from '@modrinth/ui'
import { commonMessages, defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
import HCaptcha from '@/components/ui/HCaptcha.vue'

View File

@@ -140,8 +140,13 @@ import {
RightArrowIcon,
SteamColorIcon,
} from '@modrinth/assets'
import { commonMessages, injectNotificationManager } from '@modrinth/ui'
import { IntlFormatted } from '@vintl/vintl/components'
import {
commonMessages,
defineMessages,
injectNotificationManager,
IntlFormatted,
useVIntl,
} from '@modrinth/ui'
import HCaptcha from '@/components/ui/HCaptcha.vue'
import { getAuthUrl, getLauncherRedirectUrl } from '@/composables/auth.js'

View File

@@ -145,8 +145,14 @@ import {
SteamColorIcon,
UserIcon,
} from '@modrinth/assets'
import { Checkbox, commonMessages, injectNotificationManager } from '@modrinth/ui'
import { IntlFormatted } from '@vintl/vintl/components'
import {
Checkbox,
commonMessages,
defineMessages,
injectNotificationManager,
IntlFormatted,
useVIntl,
} from '@modrinth/ui'
import HCaptcha from '@/components/ui/HCaptcha.vue'
import { getAuthUrl } from '@/composables/auth.js'

View File

@@ -64,7 +64,7 @@
</template>
<script setup>
import { RightArrowIcon, SettingsIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager } from '@modrinth/ui'
import { ButtonStyled, defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl()

View File

@@ -49,8 +49,14 @@
<script setup>
import { RightArrowIcon, WavingRinthbot } from '@modrinth/assets'
import { Checkbox, commonMessages, normalizeChildren } from '@modrinth/ui'
import { IntlFormatted } from '@vintl/vintl/components'
import {
Checkbox,
commonMessages,
defineMessages,
IntlFormatted,
normalizeChildren,
useVIntl,
} from '@modrinth/ui'
const route = useRoute()

View File

@@ -381,10 +381,13 @@ import {
commonProjectTypeCategoryMessages,
commonProjectTypeSentenceMessages,
ConfirmModal,
defineMessage,
defineMessages,
FileInput,
HorizontalRule,
injectModrinthClient,
injectNotificationManager,
IntlFormatted,
NewModal,
normalizeChildren,
NormalPage,
@@ -393,10 +396,9 @@ import {
SidebarCard,
useRelativeTime,
useSavable,
useVIntl,
} from '@modrinth/ui'
import { isAdmin } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import dayjs from 'dayjs'
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'

View File

@@ -43,7 +43,7 @@ import {
OrganizationIcon,
ReportIcon,
} from '@modrinth/assets'
import { commonMessages } from '@modrinth/ui'
import { commonMessages, useVIntl } from '@modrinth/ui'
import { type User, UserBadge } from '@modrinth/utils'
import NavStack from '~/components/ui/NavStack.vue'

View File

@@ -71,10 +71,11 @@ import {
Button,
ButtonStyled,
ConfirmModal,
defineMessages,
injectNotificationManager,
useVIntl,
} from '@modrinth/ui'
import type { AffiliateLink } from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
const createModal = useTemplateRef<typeof AffiliateLinkCreateModal>('createModal')
const revokeModal = useTemplateRef<typeof ConfirmModal>('revokeModal')

View File

@@ -106,7 +106,7 @@ import {
SearchIcon,
XIcon,
} from '@modrinth/assets'
import { Avatar, Button, commonMessages } from '@modrinth/ui'
import { Avatar, Button, commonMessages, defineMessages, useVIntl } from '@modrinth/ui'
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'

View File

@@ -335,6 +335,7 @@ import {
injectNotificationManager,
NewModal,
ProjectStatusBadge,
useVIntl,
} from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils'
import { Multiselect } from 'vue-multiselect'

View File

@@ -260,8 +260,8 @@
<script setup lang="ts">
import { ArrowUpRightIcon, InProgressIcon, UnknownIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import dayjs from 'dayjs'
import { Tooltip } from 'floating-vue'

View File

@@ -90,9 +90,8 @@ import {
GenericListIcon,
SpinnerIcon,
} from '@modrinth/assets'
import { ButtonStyled, Combobox } from '@modrinth/ui'
import { ButtonStyled, Combobox, defineMessages, useVIntl } from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import dayjs from 'dayjs'
import RevenueTransaction from '~/components/ui/dashboard/RevenueTransaction.vue'

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { commonProjectTypeCategoryMessages } from '@modrinth/ui'
import { useVIntl } from '@vintl/vintl'
import { commonProjectTypeCategoryMessages, useVIntl } from '@modrinth/ui'
import NavTabs from '~/components/ui/NavTabs.vue'
const { formatMessage } = useVIntl()
const flags = useFeatureFlags()

View File

@@ -12,6 +12,7 @@ import {
SearchIcon,
XIcon,
} from '@modrinth/assets'
import { defineMessages, useVIntl } from '@modrinth/ui'
import {
Avatar,
Button,

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { useRelativeTime } from '@modrinth/ui'
import { defineMessages, useRelativeTime, useVIntl } from '@modrinth/ui'
const vintl = useVIntl()
const { formatMessage } = vintl

View File

@@ -651,10 +651,10 @@ import {
commonMessages,
injectNotificationManager,
ModrinthServersPurchaseModal,
useVIntl,
} from '@modrinth/ui'
import { monthsInInterval } from '@modrinth/ui/src/utils/billing.ts'
import { formatPrice } from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
import { computed } from 'vue'
import { useBaseFetch } from '@/composables/fetch.js'

View File

@@ -385,8 +385,10 @@ import {
SettingsIcon,
TransferIcon,
} from '@modrinth/assets'
import type { MessageDescriptor } from '@modrinth/ui'
import {
ButtonStyled,
defineMessage,
ErrorInformationCard,
injectModrinthClient,
injectNotificationManager,
@@ -397,7 +399,6 @@ import {
} from '@modrinth/ui'
import type { PowerAction, Stats } from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import type { MessageDescriptor } from '@vintl/vintl'
import DOMPurify from 'dompurify'
import { computed, onMounted, onUnmounted, type Reactive, reactive, ref } from 'vue'

View File

@@ -439,9 +439,15 @@ import {
ModrinthIcon,
SearchIcon,
} from '@modrinth/assets'
import { Avatar, ButtonStyled, commonMessages, useRelativeTime } from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import {
Avatar,
ButtonStyled,
commonMessages,
defineMessages,
IntlFormatted,
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import { ref } from 'vue'
import { Multiselect } from 'vue-multiselect'

View File

@@ -18,8 +18,7 @@
<script setup lang="ts">
import { FolderIcon, ReportIcon, ShieldCheckIcon } from '@modrinth/assets'
import { Chips } from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { Chips, defineMessages, useVIntl } from '@modrinth/ui'
import NavTabs from '@/components/ui/NavTabs.vue'

View File

@@ -105,8 +105,15 @@ import {
SortDescIcon,
XIcon,
} from '@modrinth/assets'
import { Button, ButtonStyled, Combobox, type ComboboxOption, Pagination } from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import {
Button,
ButtonStyled,
Combobox,
type ComboboxOption,
defineMessages,
Pagination,
useVIntl,
} from '@modrinth/ui'
import Fuse from 'fuse.js'
import ConfettiExplosion from 'vue-confetti-explosion'

View File

@@ -78,9 +78,15 @@
<script setup lang="ts">
import { ListFilterIcon, SearchIcon, SortAscIcon, SortDescIcon, XIcon } from '@modrinth/assets'
import type { ExtendedReport } from '@modrinth/moderation'
import { Button, Combobox, type ComboboxOption, Pagination } from '@modrinth/ui'
import {
Button,
Combobox,
type ComboboxOption,
defineMessages,
Pagination,
useVIntl,
} from '@modrinth/ui'
import type { Report } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import Fuse from 'fuse.js'
import ReportCard from '~/components/ui/moderation/ModerationReportCard.vue'

View File

@@ -5,11 +5,12 @@ import {
Button,
Combobox,
type ComboboxOption,
defineMessages,
injectModrinthClient,
Pagination,
useVIntl,
} from '@modrinth/ui'
import { useInfiniteQuery, useQueryClient } from '@tanstack/vue-query'
import { defineMessages, useVIntl } from '@vintl/vintl'
import Fuse from 'fuse.js'
import MaliciousSummaryModal, {

View File

@@ -261,7 +261,14 @@ import {
UsersIcon,
XIcon,
} from '@modrinth/assets'
import { Avatar, ButtonStyled, commonMessages, ContentPageHeader, OverflowMenu } from '@modrinth/ui'
import {
Avatar,
ButtonStyled,
commonMessages,
ContentPageHeader,
OverflowMenu,
useVIntl,
} from '@modrinth/ui'
import type { Organization, ProjectStatus, ProjectType, ProjectV3 } from '@modrinth/utils'
import { formatNumber } from '@modrinth/utils'

View File

@@ -334,6 +334,7 @@ import {
injectNotificationManager,
NewModal,
ProjectStatusBadge,
useVIntl,
} from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils'
import { Multiselect } from 'vue-multiselect'

View File

@@ -86,7 +86,7 @@
</template>
<script setup>
import { HeartIcon, ModrinthPlusIcon, SettingsIcon, SparklesIcon, StarIcon } from '@modrinth/assets'
import { injectNotificationManager, PurchaseModal } from '@modrinth/ui'
import { injectNotificationManager, PurchaseModal, useVIntl } from '@modrinth/ui'
import { calculateSavings, formatPrice, getCurrency } from '@modrinth/utils'
import { useBaseFetch } from '@/composables/fetch.js'

View File

@@ -281,18 +281,21 @@ import {
VersionIcon,
XCircleIcon,
} from '@modrinth/assets'
import { defineMessage } from '@modrinth/ui'
import {
AutoLink,
Avatar,
ButtonStyled,
defineMessages,
injectNotificationManager,
IntlFormatted,
MarkdownEditor,
type MessageDescriptor,
RadialHeader,
RadioButtons,
useVIntl,
} from '@modrinth/ui'
import type { Project, Report, User, Version } from '@modrinth/utils'
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { useImageUpload } from '~/composables/image-upload.ts'

View File

@@ -95,7 +95,7 @@ import {
ShieldIcon,
UserIcon,
} from '@modrinth/assets'
import { commonMessages, commonSettingsMessages } from '@modrinth/ui'
import { commonMessages, commonSettingsMessages, useVIntl } from '@modrinth/ui'
import NavStack from '~/components/ui/NavStack.vue'

View File

@@ -226,6 +226,7 @@ import {
CopyCode,
FileInput,
injectNotificationManager,
useVIntl,
} from '@modrinth/ui'
import Modal from '~/components/ui/Modal.vue'

View File

@@ -95,6 +95,7 @@ import {
commonSettingsMessages,
ConfirmModal,
injectNotificationManager,
useVIntl,
} from '@modrinth/ui'
import { useScopes } from '~/composables/auth/scopes.ts'

View File

@@ -38,7 +38,7 @@
</div>
</template>
<script setup>
import { Badge, Breadcrumbs } from '@modrinth/ui'
import { Badge, Breadcrumbs, useVIntl } from '@modrinth/ui'
import { formatPrice } from '@modrinth/utils'
import { products } from '~/generated/state.json'

View File

@@ -617,11 +617,13 @@ import {
commonMessages,
ConfirmModal,
CopyCode,
defineMessages,
getPaymentMethodIcon,
injectNotificationManager,
OverflowMenu,
PurchaseModal,
ServerListing,
useVIntl,
} from '@modrinth/ui'
import { calculateSavings, formatPrice, getCurrency } from '@modrinth/utils'
import { computed, ref } from 'vue'

View File

@@ -205,10 +205,16 @@
<script setup lang="ts">
import { CodeIcon, RadioButtonCheckedIcon, RadioButtonIcon } from '@modrinth/assets'
import { Button, injectNotificationManager, normalizeChildren, ThemeSelector } from '@modrinth/ui'
import {
Button,
defineMessages,
injectNotificationManager,
IntlFormatted,
normalizeChildren,
ThemeSelector,
useVIntl,
} from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import MessageBanner from '~/components/ui/MessageBanner.vue'
import type { DisplayLocation } from '~/plugins/cosmetics'

View File

@@ -1,13 +1,18 @@
<script setup lang="ts">
import { IssuesIcon, RadioButtonCheckedIcon, RadioButtonIcon } from '@modrinth/assets'
import { Admonition, commonSettingsMessages } from '@modrinth/ui'
import { IntlFormatted } from '@vintl/vintl/components'
import { RadioButtonCheckedIcon, RadioButtonIcon } from '@modrinth/assets'
import {
Admonition,
commonSettingsMessages,
defineMessages,
IntlFormatted,
useVIntl,
} from '@modrinth/ui'
import Fuse from 'fuse.js/dist/fuse.basic'
import { isModifierKeyDown } from '~/helpers/events.ts'
const vintl = useVIntl()
const { formatMessage } = vintl
const { formatMessage } = useVIntl()
const { locale, setLocale, locales } = useI18n()
const messages = defineMessages({
languagesDescription: {
@@ -23,10 +28,6 @@ const messages = defineMessages({
id: 'settings.language.languages.search.no-results',
defaultMessage: 'No languages match your search.',
},
searchFieldDescription: {
id: 'settings.language.languages.search-field.description',
defaultMessage: 'Submit to focus the first search result',
},
searchFieldPlaceholder: {
id: 'settings.language.languages.search-field.placeholder',
defaultMessage: 'Search for a language...',
@@ -36,18 +37,6 @@ const messages = defineMessages({
defaultMessage:
'{matches, plural, =0 {No languages match} one {# language matches} other {# languages match}} your search.',
},
loadFailed: {
id: 'settings.language.languages.load-failed',
defaultMessage: 'Cannot load this language. Try again in a bit.',
},
languageLabelApplying: {
id: 'settings.language.languages.language-label-applying',
defaultMessage: '{label}. Applying...',
},
languageLabelError: {
id: 'settings.language.languages.language-label-error',
defaultMessage: '{label}. Error',
},
languageWarning: {
id: 'settings.language.warning',
defaultMessage:
@@ -56,22 +45,10 @@ const messages = defineMessages({
})
const categoryNames = defineMessages({
auto: {
id: 'settings.language.categories.auto',
defaultMessage: 'Automatic',
},
default: {
id: 'settings.language.categories.default',
defaultMessage: 'Standard languages',
},
fun: {
id: 'settings.language.categories.fun',
defaultMessage: 'Fun languages',
},
experimental: {
id: 'settings.language.categories.experimental',
defaultMessage: 'Experimental languages',
},
searchResult: {
id: 'settings.language.categories.search-result',
defaultMessage: 'Search results',
@@ -80,97 +57,46 @@ const categoryNames = defineMessages({
type Category = keyof typeof categoryNames
const categoryOrder: Category[] = ['auto', 'default', 'fun', 'experimental']
function normalizeCategoryName(name?: string): keyof typeof categoryNames {
switch (name) {
case 'auto':
case 'fun':
case 'experimental':
return name
default:
return 'default'
}
}
type LocaleBase = {
type LocaleInfo = {
category: Category
tag: string
displayName: string
nativeName: string
searchTerms?: string[]
}
type AutomaticLocale = LocaleBase & {
auto: true
}
type CommonLocale = LocaleBase & {
auto?: never
displayName: string
defaultName: string
translatedName: string
}
type Locale = AutomaticLocale | CommonLocale
const $defaultNames = useDisplayNames(() => vintl.defaultLocale)
const $translatedNames = useDisplayNames(() => vintl.locale)
const displayNames = new Intl.DisplayNames(['en'], { type: 'language' })
const $locales = computed(() => {
const locales: Locale[] = []
const result: LocaleInfo[] = []
locales.push({
auto: true,
tag: 'auto',
category: 'auto',
searchTerms: [
'automatic',
'Sync with the system language',
formatMessage(messages.automaticLocale),
],
})
const localeList = Array.isArray(locales.value) ? locales.value : Object.keys(locales.value)
for (const locale of vintl.availableLocales) {
let displayName = locale.meta?.displayName
for (const loc of localeList) {
const tag = typeof loc === 'string' ? loc : loc.code
const name = typeof loc === 'object' && loc.name ? loc.name : (displayNames.of(tag) ?? tag)
if (displayName == null) {
displayName = createDisplayNames(locale.tag).of(locale.tag) ?? locale.tag
}
const nativeDisplayNames = new Intl.DisplayNames([tag], { type: 'language' })
const nativeName = nativeDisplayNames.of(tag) ?? tag
let defaultName = vintl.defaultResources['languages.json']?.[locale.tag]
if (defaultName == null) {
defaultName = $defaultNames.value.of(locale.tag) ?? locale.tag
}
let translatedName = vintl.resources['languages.json']?.[locale.tag]
if (translatedName == null) {
translatedName = $translatedNames.value.of(locale.tag) ?? locale.tag
}
let searchTerms = locale.meta?.searchTerms
if (searchTerms === '-') searchTerms = undefined
locales.push({
tag: locale.tag,
category: normalizeCategoryName(locale.meta?.category),
displayName,
defaultName,
translatedName,
searchTerms: searchTerms?.split('\n'),
result.push({
tag,
category: 'default',
displayName: name,
nativeName,
searchTerms: [tag, name, nativeName],
})
}
return locales
return result
})
const $query = ref('')
const isQueryEmpty = () => $query.value.trim().length === 0
const fuse = new Fuse<Locale>([], {
keys: ['tag', 'displayName', 'translatedName', 'englishName', 'searchTerms'],
const fuse = new Fuse<LocaleInfo>([], {
keys: ['tag', 'displayName', 'nativeName', 'searchTerms'],
threshold: 0.4,
distance: 100,
})
@@ -178,32 +104,13 @@ const fuse = new Fuse<Locale>([], {
watchSyncEffect(() => fuse.setCollection($locales.value))
const $categories = computed(() => {
const categories = new Map<Category, Locale[]>()
for (const category of categoryOrder) categories.set(category, [])
for (const locale of $locales.value) {
let categoryLocales = categories.get(locale.category)
if (categoryLocales == null) {
categoryLocales = []
categories.set(locale.category, categoryLocales)
}
categoryLocales.push(locale)
}
for (const categoryKey of [...categories.keys()]) {
if (categories.get(categoryKey)?.length === 0) {
categories.delete(categoryKey)
}
}
const categories = new Map<Category, LocaleInfo[]>()
categories.set('default', $locales.value)
return categories
})
const $searchResults = computed(() => {
return new Map<Category, Locale[]>([
return new Map<Category, LocaleInfo[]>([
['searchResult', isQueryEmpty() ? [] : fuse.search($query.value).map(({ item }) => item)],
])
})
@@ -216,11 +123,9 @@ const $changingTo = ref<string | undefined>()
const isChanging = () => $changingTo.value != null
const $failedLocale = ref<string>()
const $activeLocale = computed(() => {
if ($changingTo.value != null) return $changingTo.value
return vintl.automatic ? 'auto' : vintl.locale
return locale.value
})
async function changeLocale(value: string) {
@@ -229,10 +134,7 @@ async function changeLocale(value: string) {
$changingTo.value = value
try {
await vintl.changeLocale(value)
$failedLocale.value = undefined
} catch {
$failedLocale.value = value
await setLocale(value)
} finally {
$changingTo.value = undefined
}
@@ -250,7 +152,7 @@ function onSearchKeydown(e: KeyboardEvent) {
focusableTarget?.focus()
}
function onItemKeydown(e: KeyboardEvent, locale: Locale) {
function onItemKeydown(e: KeyboardEvent, loc: LocaleInfo) {
switch (e.key) {
case 'Enter':
case ' ':
@@ -261,29 +163,17 @@ function onItemKeydown(e: KeyboardEvent, locale: Locale) {
if (isModifierKeyDown(e) || isChanging()) return
changeLocale(locale.tag)
changeLocale(loc.tag)
}
function onItemClick(e: MouseEvent, locale: Locale) {
function onItemClick(e: MouseEvent, loc: LocaleInfo) {
if (isModifierKeyDown(e) || isChanging()) return
changeLocale(locale.tag)
changeLocale(loc.tag)
}
function getItemLabel(locale: Locale) {
const label = locale.auto
? formatMessage(messages.automaticLocale)
: `${locale.translatedName}. ${locale.displayName}`
if ($changingTo.value === locale.tag) {
return formatMessage(messages.languageLabelApplying, { label })
}
if ($failedLocale.value === locale.tag) {
return formatMessage(messages.languageLabelError, { label })
}
return label
function getItemLabel(loc: LocaleInfo) {
return `${loc.nativeName}. ${loc.displayName}`
}
</script>
@@ -298,7 +188,7 @@ function getItemLabel(locale: Locale) {
<div class="card-description mt-4">
<IntlFormatted :message-id="messages.languagesDescription">
<template #crowdin-link="{ children }">
<template #~crowdin-link="{ children }">
<a href="https://translate.modrinth.com">
<component :is="() => children" />
</a>
@@ -306,7 +196,7 @@ function getItemLabel(locale: Locale) {
</IntlFormatted>
</div>
<div class="search-container">
<div v-if="$locales.length > 1" class="search-container">
<input
id="language-search"
v-model="$query"
@@ -314,15 +204,10 @@ function getItemLabel(locale: Locale) {
type="search"
:placeholder="formatMessage(messages.searchFieldPlaceholder)"
class="language-search"
aria-describedby="language-search-description"
:disabled="isChanging()"
@keydown="onSearchKeydown"
/>
<div id="language-search-description" class="visually-hidden">
{{ formatMessage(messages.searchFieldDescription) }}
</div>
<div id="language-search-results-announcements" class="visually-hidden" aria-live="polite">
{{
isQueryEmpty()
@@ -335,59 +220,46 @@ function getItemLabel(locale: Locale) {
</div>
<div ref="$languagesList" class="languages-list">
<template v-for="[category, locales] in $displayCategories" :key="category">
<template v-for="[category, categoryLocales] in $displayCategories" :key="category">
<strong class="category-name">
{{ formatMessage(categoryNames[category]) }}
</strong>
<div
v-if="category === 'searchResult' && locales.length === 0"
v-if="category === 'searchResult' && categoryLocales.length === 0"
class="no-results"
tabindex="0"
>
{{ formatMessage(messages.noResults) }}
</div>
<template v-for="locale in locales" :key="locale.tag">
<template v-for="loc in categoryLocales" :key="loc.tag">
<div
role="button"
:aria-pressed="$activeLocale === locale.tag"
:aria-pressed="$activeLocale === loc.tag"
:class="{
'language-item': true,
pending: $changingTo == locale.tag,
errored: $failedLocale == locale.tag,
pending: $changingTo === loc.tag,
}"
:aria-describedby="
$failedLocale == locale.tag ? `language__${locale.tag}__fail` : undefined
"
:aria-disabled="isChanging() && $changingTo !== locale.tag"
:aria-disabled="isChanging() && $changingTo !== loc.tag"
:tabindex="0"
:aria-label="getItemLabel(locale)"
@click="(e) => onItemClick(e, locale)"
@keydown="(e) => onItemKeydown(e, locale)"
:aria-label="getItemLabel(loc)"
@click="(e) => onItemClick(e, loc)"
@keydown="(e) => onItemKeydown(e, loc)"
>
<RadioButtonCheckedIcon v-if="$activeLocale === locale.tag" class="radio" />
<RadioButtonCheckedIcon v-if="$activeLocale === loc.tag" class="radio" />
<RadioButtonIcon v-else class="radio" />
<div class="language-names">
<div class="language-name">
{{ locale.auto ? formatMessage(messages.automaticLocale) : locale.displayName }}
{{ loc.displayName }}
</div>
<div v-if="!locale.auto" class="language-translated-name">
{{ locale.translatedName }}
<div class="language-translated-name">
{{ loc.nativeName }}
</div>
</div>
</div>
<div
v-if="$failedLocale === locale.tag"
:id="`language__${locale.tag}__fail`"
class="language-load-error"
>
<IssuesIcon />
{{ formatMessage(messages.loadFailed) }}
</div>
</template>
</template>
</div>
@@ -423,14 +295,6 @@ function getItemLabel(locale: Locale) {
outline: 2px solid var(--color-brand);
}
&.errored {
border-color: var(--color-red);
&:hover {
border-color: var(--color-red);
}
}
&.pending::after {
content: '';
position: absolute;
@@ -482,15 +346,6 @@ function getItemLabel(locale: Locale) {
}
}
.language-load-error {
color: var(--color-red);
font-size: var(--font-size-sm);
margin-left: 0.3rem;
display: flex;
align-items: center;
gap: 0.3rem;
}
.radio {
width: 24px;
height: 24px;
@@ -534,4 +389,9 @@ function getItemLabel(locale: Locale) {
.category-name {
margin-top: var(--spacing-card-md);
}
.no-results {
padding: var(--spacing-card-md);
color: var(--color-text-secondary);
}
</style>

View File

@@ -209,10 +209,12 @@ import {
commonSettingsMessages,
ConfirmModal,
CopyCode,
defineMessages,
injectNotificationManager,
IntlFormatted,
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import { IntlFormatted } from '@vintl/vintl/components'
import Modal from '~/components/ui/Modal.vue'
import {

View File

@@ -91,8 +91,16 @@
<script setup>
import { SaveIcon, TrashIcon, UndoIcon, UploadIcon, UserIcon, XIcon } from '@modrinth/assets'
import { Avatar, Button, commonMessages, FileInput, injectNotificationManager } from '@modrinth/ui'
import { IntlFormatted } from '@vintl/vintl/components'
import {
Avatar,
Button,
commonMessages,
defineMessages,
FileInput,
injectNotificationManager,
IntlFormatted,
useVIntl,
} from '@modrinth/ui'
const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl()

View File

@@ -60,8 +60,10 @@ import { XIcon } from '@modrinth/assets'
import {
commonMessages,
commonSettingsMessages,
defineMessages,
injectNotificationManager,
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
definePageMeta({

View File

@@ -482,14 +482,16 @@ import {
Combobox,
commonMessages,
ContentPageHeader,
defineMessages,
injectNotificationManager,
IntlFormatted,
NewModal,
OverflowMenu,
TagItem,
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import { isAdmin, isStaff, UserBadge } from '@modrinth/utils'
import { IntlFormatted } from '@vintl/vintl/components'
import TenMClubBadge from '~/assets/images/badges/10m-club.svg?component'
import AlphaTesterBadge from '~/assets/images/badges/alpha-tester.svg?component'

View File

@@ -6,8 +6,8 @@ import {
PayPalColorIcon,
VenmoColorIcon,
} from '@modrinth/assets'
import type { MessageDescriptor } from '@modrinth/ui'
import { createContext, getCurrencyIcon, paymentMethodMessages, useDebugLogger } from '@modrinth/ui'
import type { MessageDescriptor } from '@vintl/vintl'
import { type Component, computed, type ComputedRef, type Ref, ref } from 'vue'
import { getRailConfig } from '@/utils/muralpay-rails'

View File

@@ -1,21 +1,13 @@
import '@vintl/vintl'
import type { CompactNumber } from '@vintl/compact-number/dist/index.mjs'
declare global {
namespace VueIntlController {
interface MessageValueTypes {
compactNumber: CompactNumber
}
interface LocaleResources {
'languages.json'?: Partial<Record<string, string>>
}
interface LocaleResources {
'languages.json'?: Partial<Record<string, string>>
}
interface LocaleMeta {
displayName?: string
category?: string
searchTerms?: string
}
interface LocaleMeta {
displayName?: string
category?: string
searchTerms?: string
}
}
export {}

View File

@@ -1,3 +1,5 @@
import { defineMessages } from '@modrinth/ui'
const projectTypeMessages = defineMessages({
datapack: {
id: 'project-type.datapack.singular',

View File

@@ -1,4 +1,4 @@
import { defineMessage, type MessageDescriptor } from '@vintl/vintl'
import { defineMessage, type MessageDescriptor } from '@modrinth/ui'
export type FieldType = 'text' | 'select' | 'email' | 'tel' | 'date'