From c60851f80bd925c4d4010d2553f2d7daed3ad5e0 Mon Sep 17 00:00:00 2001
From: Prospector <6166773+Prospector@users.noreply.github.com>
Date: Wed, 24 Dec 2025 14:05:23 -0800
Subject: [PATCH 01/10] sample languages refactor
---
apps/app-frontend/package.json | 3 +-
apps/app-frontend/src/App.vue | 2 +-
.../src/components/ui/IntlFormatted.vue | 70 +
.../src/components/ui/UpdateToast.vue | 2 +-
.../src/components/ui/friends/FriendsList.vue | 4 +-
.../components/ui/friends/FriendsSection.vue | 2 +-
.../ui/instance_settings/GeneralSettings.vue | 2 +-
.../ui/instance_settings/HooksSettings.vue | 2 +-
.../InstallationSettings.vue | 2 +-
.../ui/instance_settings/JavaSettings.vue | 2 +-
.../ui/instance_settings/WindowSettings.vue | 2 +-
.../components/ui/modal/AppSettingsModal.vue | 2 +-
.../ui/modal/InstanceSettingsModal.vue | 2 +-
.../src/components/ui/world/InstanceItem.vue | 2 +-
.../src/components/ui/world/WorldItem.vue | 4 +-
.../ui/world/modal/AddServerModal.vue | 2 +-
.../ui/world/modal/EditServerModal.vue | 2 +-
.../modal/EditSingleplayerWorldModal.vue | 2 +-
.../ui/world/modal/HideFromHomeOption.vue | 3 +-
.../ui/world/modal/ServerModalBody.vue | 2 +-
apps/app-frontend/src/i18n.ts | 37 +
apps/app-frontend/src/main.js | 21 +-
apps/app-frontend/src/pages/Browse.vue | 2 +-
apps/app-frontend/src/pages/instance/Mods.vue | 2 +-
.../src/pages/instance/Worlds.vue | 2 +-
apps/app-frontend/src/utils/i18n-vintl.ts | 53 +
apps/frontend/nuxt.config.ts | 246 +--
apps/frontend/package.json | 6 +-
.../src/components/ui/IntlFormatted.vue | 69 +
.../src/components/ui/ProjectMemberHeader.vue | 2 +-
.../ui/create/CollectionCreateModal.vue | 3 +-
.../components/ui/create/CreateLimitAlert.vue | 3 +-
.../ui/create/OrganizationCreateModal.vue | 3 +-
.../ui/create/ProjectCreateModal.vue | 3 +-
.../ui/dashboard/CreatorTaxFormModal.vue | 4 +-
.../ui/dashboard/CreatorWithdrawModal.vue | 2 +-
.../ui/dashboard/RevenueInputField.vue | 3 +-
.../ui/dashboard/WithdrawFeeBreakdown.vue | 3 +-
.../withdraw-stages/CompletionStage.vue | 4 +-
.../LegacyPaypalDetailsStage.vue | 4 +-
.../withdraw-stages/MethodSelectionStage.vue | 4 +-
.../withdraw-stages/MuralpayDetailsStage.vue | 4 +-
.../withdraw-stages/MuralpayKycStage.vue | 2 +-
.../withdraw-stages/TaxFormStage.vue | 4 +-
.../TremendousDetailsStage.vue | 4 +-
.../ui/moderation/ModerationProjectNags.vue | 3 +-
.../src/components/ui/news/LatestNewsRow.vue | 3 +-
.../servers/marketing/ServerPlanSelector.vue | 3 +-
.../ui/servers/notice/NoticeDashboardItem.vue | 3 +-
apps/frontend/src/error.vue | 4 +-
apps/frontend/src/i18n.config.ts | 59 +
apps/frontend/src/layouts/default.vue | 2 +-
apps/frontend/src/pages/[type]/[id].vue | 2 +-
.../src/pages/[type]/[id]/settings.vue | 2 +-
.../[type]/[id]/settings/environment.vue | 3 +-
.../pages/[type]/[id]/settings/general.vue | 3 +-
.../src/pages/admin/servers/notices.vue | 2 +-
apps/frontend/src/pages/app.vue | 4 +-
apps/frontend/src/pages/auth/authorize.vue | 2 +-
apps/frontend/src/pages/auth/sign-in.vue | 2 +-
apps/frontend/src/pages/auth/sign-up.vue | 2 +-
apps/frontend/src/pages/auth/welcome.vue | 3 +-
apps/frontend/src/pages/collection/[id].vue | 4 +-
.../src/pages/dashboard/affiliate-links.vue | 3 +-
.../src/pages/dashboard/revenue/index.vue | 2 +-
.../src/pages/dashboard/revenue/transfers.vue | 2 +-
apps/frontend/src/pages/discover.vue | 2 +-
apps/frontend/src/pages/hosting/index.vue | 2 +-
.../src/pages/hosting/manage/[id].vue | 2 +-
apps/frontend/src/pages/index.vue | 4 +-
apps/frontend/src/pages/moderation.vue | 2 +-
apps/frontend/src/pages/moderation/index.vue | 2 +-
.../src/pages/moderation/reports/index.vue | 2 +-
.../src/pages/moderation/technical-review.vue | 2 +-
apps/frontend/src/pages/report.vue | 4 +-
apps/frontend/src/pages/settings/index.vue | 4 +-
apps/frontend/src/pages/settings/language.vue | 255 +--
apps/frontend/src/pages/settings/pats.vue | 2 +-
apps/frontend/src/pages/settings/profile.vue | 3 +-
apps/frontend/src/pages/user/[id].vue | 2 +-
apps/frontend/src/plugins/i18n-bridge.ts | 44 +
apps/frontend/src/plugins/locale-loader.ts | 49 +
.../src/providers/creator-withdraw.ts | 2 +-
apps/frontend/src/types/vintl.d.ts | 26 +-
apps/frontend/src/utils/i18n-vintl.ts | 53 +
apps/frontend/src/utils/muralpay-rails.ts | 2 +-
packages/moderation/package.json | 4 +-
packages/moderation/src/data/nags/core.ts | 2 +-
.../moderation/src/data/nags/description.ts | 2 +-
packages/moderation/src/data/nags/links.ts | 2 +-
packages/moderation/src/data/nags/tags.ts | 2 +-
packages/moderation/src/types/nags.ts | 2 +-
packages/ui/package.json | 4 +-
.../affiliate/AffiliateLinkCard.vue | 2 +-
.../affiliate/AffiliateLinkCreateModal.vue | 2 +-
packages/ui/src/components/base/Badge.vue | 2 +-
packages/ui/src/components/base/CopyCode.vue | 2 +-
.../components/base/EnvironmentIndicator.vue | 2 +-
packages/ui/src/components/base/FilterBar.vue | 2 +-
.../ui/src/components/base/IconSelect.vue | 2 +-
.../ui/src/components/base/IntlFormatted.vue | 74 +
.../ui/src/components/base/ServerNotice.vue | 2 +-
.../ui/src/components/base/SettingsLabel.vue | 4 +-
.../components/base/UnsavedChangesPopup.vue | 2 +-
packages/ui/src/components/base/index.ts | 1 +
.../billing/AddPaymentMethodModal.vue | 2 +-
.../billing/ExpandableInvoiceTotal.vue | 2 +-
.../billing/FormattedPaymentMethod.vue | 2 +-
.../billing/ModalBasedServerPlan.vue | 2 +-
.../billing/ModrinthServersPurchaseModal.vue | 2 +-
.../src/components/billing/PurchaseModal.vue | 2 +-
.../billing/ServersPurchase0Plan.vue | 2 +-
.../billing/ServersPurchase1Region.vue | 4 +-
.../billing/ServersPurchase2PaymentMethod.vue | 2 +-
.../billing/ServersPurchase3Review.vue | 2 +-
.../billing/ServersRegionButton.vue | 2 +-
.../components/changelog/ChangelogEntry.vue | 2 +-
.../ui/src/components/modal/TabbedModal.vue | 2 +-
.../project/ProjectPageVersions.vue | 2 +-
.../project/ProjectSidebarCompatibility.vue | 7 +-
.../project/ProjectSidebarCreators.vue | 2 +-
.../project/ProjectSidebarDetails.vue | 2 +-
.../project/ProjectSidebarLinks.vue | 2 +-
.../components/project/ProjectStatusBadge.vue | 2 +-
.../ProjectSettingsEnvSelector.vue | 2 +-
.../components/search/SearchFilterControl.vue | 2 +-
.../components/search/SearchSidebarFilter.vue | 2 +-
.../components/servers/backups/BackupItem.vue | 2 +-
.../src/components/settings/ThemeSelector.vue | 2 +-
.../version/VersionChannelIndicator.vue | 2 +-
packages/ui/src/composables/how-ago.ts | 66 +-
packages/ui/src/utils/common-messages.ts | 2 +-
packages/ui/src/utils/game-modes.ts | 3 +-
packages/ui/src/utils/i18n.ts | 82 +
packages/ui/src/utils/index.ts | 1 +
packages/ui/src/utils/notices.ts | 2 +-
packages/ui/src/utils/regions.ts | 2 +-
packages/ui/src/utils/search.ts | 3 +-
pnpm-lock.yaml | 1660 ++++-------------
139 files changed, 1265 insertions(+), 1909 deletions(-)
create mode 100644 apps/app-frontend/src/components/ui/IntlFormatted.vue
create mode 100644 apps/app-frontend/src/i18n.ts
create mode 100644 apps/app-frontend/src/utils/i18n-vintl.ts
create mode 100644 apps/frontend/src/components/ui/IntlFormatted.vue
create mode 100644 apps/frontend/src/i18n.config.ts
create mode 100644 apps/frontend/src/plugins/i18n-bridge.ts
create mode 100644 apps/frontend/src/plugins/locale-loader.ts
create mode 100644 apps/frontend/src/utils/i18n-vintl.ts
create mode 100644 packages/ui/src/components/base/IntlFormatted.vue
create mode 100644 packages/ui/src/utils/i18n.ts
diff --git a/apps/app-frontend/package.json b/apps/app-frontend/package.json
index 44439c7976..a0468e289a 100644
--- a/apps/app-frontend/package.json
+++ b/apps/app-frontend/package.json
@@ -28,7 +28,8 @@
"@tauri-apps/plugin-updater": "^2.7.1",
"@tauri-apps/plugin-window-state": "^2.2.2",
"@types/three": "^0.172.0",
- "@vintl/vintl": "^4.4.1",
+ "intl-messageformat": "^10.7.7",
+ "vue-i18n": "^9.14.0",
"@vueuse/core": "^11.1.0",
"dayjs": "^1.11.10",
"floating-vue": "^5.2.2",
diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue
index 26f041fd68..4a56f35eed 100644
--- a/apps/app-frontend/src/App.vue
+++ b/apps/app-frontend/src/App.vue
@@ -48,7 +48,6 @@ import { getCurrentWindow } from '@tauri-apps/api/window'
import { openUrl } from '@tauri-apps/plugin-opener'
import { type } from '@tauri-apps/plugin-os'
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
-import { defineMessages, useVIntl } from '@vintl/vintl'
import { $fetch } from 'ofetch'
import { computed, onMounted, onUnmounted, provide, ref, watch } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router'
@@ -98,6 +97,7 @@ import {
import { useError } from '@/store/error.js'
import { useInstall } from '@/store/install.js'
import { useLoading, useTheming } from '@/store/state'
+import { defineMessages, useVIntl } from '@/utils/i18n-vintl'
import { create_profile_and_install_from_file } from './helpers/pack'
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
diff --git a/apps/app-frontend/src/components/ui/IntlFormatted.vue b/apps/app-frontend/src/components/ui/IntlFormatted.vue
new file mode 100644
index 0000000000..77a4d29c36
--- /dev/null
+++ b/apps/app-frontend/src/components/ui/IntlFormatted.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+ {{ part }}
+
+
diff --git a/apps/app-frontend/src/components/ui/UpdateToast.vue b/apps/app-frontend/src/components/ui/UpdateToast.vue
index af9676080d..95e4f5957b 100644
--- a/apps/app-frontend/src/components/ui/UpdateToast.vue
+++ b/apps/app-frontend/src/components/ui/UpdateToast.vue
@@ -2,10 +2,10 @@
import { DownloadIcon, ExternalIcon, RefreshCwIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, commonMessages, ProgressBar } from '@modrinth/ui'
import { formatBytes } from '@modrinth/utils'
-import { defineMessages, useVIntl } from '@vintl/vintl'
import { ref } from 'vue'
import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts'
+import { defineMessages, useVIntl } from '@/utils/i18n-vintl'
const { formatMessage } = useVIntl()
diff --git a/apps/app-frontend/src/components/ui/friends/FriendsList.vue b/apps/app-frontend/src/components/ui/friends/FriendsList.vue
index 991e7e7d69..293e6828e5 100644
--- a/apps/app-frontend/src/components/ui/friends/FriendsList.vue
+++ b/apps/app-frontend/src/components/ui/friends/FriendsList.vue
@@ -7,11 +7,10 @@ import {
injectNotificationManager,
useRelativeTime,
} from '@modrinth/ui'
-import { defineMessages, useVIntl } from '@vintl/vintl'
-import { IntlFormatted } from '@vintl/vintl/components'
import { computed, onUnmounted, ref, watch } from 'vue'
import FriendsSection from '@/components/ui/friends/FriendsSection.vue'
+import IntlFormatted from '@/components/ui/IntlFormatted.vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { friend_listener } from '@/helpers/events'
import {
@@ -22,6 +21,7 @@ import {
transformFriends,
} from '@/helpers/friends.ts'
import type { ModrinthCredentials } from '@/helpers/mr_auth'
+import { defineMessages, useVIntl } from '@/utils/i18n-vintl'
const { formatMessage } = useVIntl()
diff --git a/apps/app-frontend/src/components/ui/friends/FriendsSection.vue b/apps/app-frontend/src/components/ui/friends/FriendsSection.vue
index 83e673f7c0..da927fbe9d 100644
--- a/apps/app-frontend/src/components/ui/friends/FriendsSection.vue
+++ b/apps/app-frontend/src/components/ui/friends/FriendsSection.vue
@@ -2,11 +2,11 @@
import { MoreVerticalIcon, TrashIcon, UserIcon, XIcon } from '@modrinth/assets'
import { Accordion, Avatar, ButtonStyled, OverflowMenu } from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener'
-import { defineMessages, useVIntl } from '@vintl/vintl'
import { useTemplateRef } from 'vue'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import type { FriendWithUserData } from '@/helpers/friends.ts'
+import { defineMessages, useVIntl } from '@/utils/i18n-vintl'
const { formatMessage } = useVIntl()
diff --git a/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue b/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue
index b98d00cf2b..8cc209d8c6 100644
--- a/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue
+++ b/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue
@@ -9,13 +9,13 @@ import {
} from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { open } from '@tauri-apps/plugin-dialog'
-import { defineMessages, useVIntl } from '@vintl/vintl'
import { computed, type Ref, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
+import { defineMessages, useVIntl } from '@/utils/i18n-vintl'
import type { GameInstance, InstanceSettingsTabProps } from '../../../helpers/types'
diff --git a/apps/app-frontend/src/components/ui/instance_settings/HooksSettings.vue b/apps/app-frontend/src/components/ui/instance_settings/HooksSettings.vue
index 9f1d589beb..9443d5b4d4 100644
--- a/apps/app-frontend/src/components/ui/instance_settings/HooksSettings.vue
+++ b/apps/app-frontend/src/components/ui/instance_settings/HooksSettings.vue
@@ -1,10 +1,10 @@
+
+
+
+
+ {{ part }}
+
+
diff --git a/apps/frontend/src/components/ui/ProjectMemberHeader.vue b/apps/frontend/src/components/ui/ProjectMemberHeader.vue
index be287995fb..a3c2e13c5d 100644
--- a/apps/frontend/src/components/ui/ProjectMemberHeader.vue
+++ b/apps/frontend/src/components/ui/ProjectMemberHeader.vue
@@ -26,10 +26,10 @@
import { CheckIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager } 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'
+import { defineMessages, type MessageDescriptor, useVIntl } from '~/utils/i18n-vintl'
const { addNotification } = injectNotificationManager()
diff --git a/apps/frontend/src/components/ui/create/CollectionCreateModal.vue b/apps/frontend/src/components/ui/create/CollectionCreateModal.vue
index 9eada58306..f822fd0ffc 100644
--- a/apps/frontend/src/components/ui/create/CollectionCreateModal.vue
+++ b/apps/frontend/src/components/ui/create/CollectionCreateModal.vue
@@ -59,7 +59,8 @@
@@ -298,7 +183,7 @@ function getItemLabel(locale: Locale) {
-
+
@@ -306,7 +191,7 @@ function getItemLabel(locale: Locale) {
-
+
-
- {{ formatMessage(messages.searchFieldDescription) }}
-
-
{{
isQueryEmpty()
@@ -335,59 +215,46 @@ function getItemLabel(locale: Locale) {
-
+
{{ formatMessage(categoryNames[category]) }}
{{ formatMessage(messages.noResults) }}
-
+
onItemClick(e, locale)"
- @keydown="(e) => onItemKeydown(e, locale)"
+ :aria-label="getItemLabel(loc)"
+ @click="(e) => onItemClick(e, loc)"
+ @keydown="(e) => onItemKeydown(e, loc)"
>
-
+
- {{ locale.auto ? formatMessage(messages.automaticLocale) : locale.displayName }}
+ {{ loc.displayName }}
-
- {{ locale.translatedName }}
+
+ {{ loc.nativeName }}
-
-
-
- {{ formatMessage(messages.loadFailed) }}
-
@@ -423,14 +290,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 +341,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 +384,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);
+}
diff --git a/apps/frontend/src/pages/settings/pats.vue b/apps/frontend/src/pages/settings/pats.vue
index bf55bb70e6..9224dac69a 100644
--- a/apps/frontend/src/pages/settings/pats.vue
+++ b/apps/frontend/src/pages/settings/pats.vue
@@ -212,8 +212,8 @@ import {
injectNotificationManager,
useRelativeTime,
} from '@modrinth/ui'
-import { IntlFormatted } from '@vintl/vintl/components'
+import IntlFormatted from '~/components/ui/IntlFormatted.vue'
import Modal from '~/components/ui/Modal.vue'
import {
getScopeValue,
diff --git a/apps/frontend/src/pages/settings/profile.vue b/apps/frontend/src/pages/settings/profile.vue
index 12be87ddba..db407c3150 100644
--- a/apps/frontend/src/pages/settings/profile.vue
+++ b/apps/frontend/src/pages/settings/profile.vue
@@ -92,7 +92,8 @@
+
+
+
+
+ {{ part }}
+
+
diff --git a/packages/ui/src/components/base/ServerNotice.vue b/packages/ui/src/components/base/ServerNotice.vue
index ba1daadd81..caf56567f6 100644
--- a/packages/ui/src/components/base/ServerNotice.vue
+++ b/packages/ui/src/components/base/ServerNotice.vue
@@ -33,8 +33,8 @@
-
-
-
-
- {{ part }}
-
-
diff --git a/apps/app-frontend/src/i18n.ts b/apps/app-frontend/src/i18n.ts
index d085ce180f..219981313b 100644
--- a/apps/app-frontend/src/i18n.ts
+++ b/apps/app-frontend/src/i18n.ts
@@ -1,37 +1,18 @@
-import IntlMessageFormat from 'intl-messageformat'
-import { type CompileError, createI18n, type MessageCompiler, type MessageContext } from 'vue-i18n'
+import { buildLocaleMessages, createMessageCompiler, type CrowdinMessages } from '@modrinth/ui'
+import { createI18n } from 'vue-i18n'
-const messageCompiler: MessageCompiler = (msg, { locale, key, onError }) => {
- if (typeof msg === 'string') {
- try {
- const formatter = new IntlMessageFormat(msg, locale)
- return (ctx: MessageContext) => {
- try {
- return formatter.format(ctx.values as Record) as string
- } catch {
- return String(msg)
- }
- }
- } catch (e) {
- onError?.(e as CompileError)
- return () => key
- }
- } else {
- onError?.(new Error('AST not supported') as CompileError)
- return () => key
- }
-}
+const localeModules = import.meta.glob<{ default: CrowdinMessages }>('./locales/*/index.json', {
+ eager: true,
+})
const i18n = createI18n({
legacy: false,
locale: 'en-US',
fallbackLocale: 'en-US',
- messageCompiler,
+ messageCompiler: createMessageCompiler(),
missingWarn: false,
fallbackWarn: false,
- messages: {
- 'en-US': {},
- },
+ messages: buildLocaleMessages(localeModules),
})
export default i18n
diff --git a/apps/app-frontend/src/utils/i18n-vintl.ts b/apps/app-frontend/src/utils/i18n-vintl.ts
deleted file mode 100644
index 0b8c971dda..0000000000
--- a/apps/app-frontend/src/utils/i18n-vintl.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import IntlMessageFormat from 'intl-messageformat'
-import { useI18n } from 'vue-i18n'
-
-export interface MessageDescriptor {
- id: string
- defaultMessage?: string
- description?: string
-}
-
-export type MessageDescriptorMap = Record
-
-export function defineMessage(descriptor: T): T {
- return descriptor
-}
-
-export function defineMessages>(
- descriptors: T,
-): T {
- return descriptors
-}
-
-export interface VIntlFormatters {
- formatMessage(descriptor: MessageDescriptor, values?: Record): string
-}
-
-export function useVIntl(): VIntlFormatters & { locale: globalThis.Ref } {
- const { t, locale, messages } = useI18n()
-
- function formatMessage(descriptor: MessageDescriptor, values?: Record): string {
- const key = descriptor.id
- const localeMessages = messages.value[locale.value] as Record | undefined
- const translation = localeMessages?.[key] ?? (t(key, values ?? {}) as string)
-
- if (translation && translation !== key) {
- try {
- const formatter = new IntlMessageFormat(translation, locale.value)
- return formatter.format(values ?? {}) as string
- } catch {
- return translation
- }
- }
-
- const defaultMsg = descriptor.defaultMessage ?? key
- try {
- const formatter = new IntlMessageFormat(defaultMsg, locale.value)
- return formatter.format(values ?? {}) as string
- } catch {
- return defaultMsg
- }
- }
-
- return { formatMessage, locale }
-}
diff --git a/apps/frontend/nuxt.config.ts b/apps/frontend/nuxt.config.ts
index c092a576ac..b0432298f3 100644
--- a/apps/frontend/nuxt.config.ts
+++ b/apps/frontend/nuxt.config.ts
@@ -1,4 +1,5 @@
import { GenericModrinthClient, type Labrinth } from '@modrinth/api-client'
+import { LOCALES } from '@modrinth/ui'
import serverSidedVue from '@vitejs/plugin-vue'
import { promises as fs } from 'fs'
import { defineNuxtConfig } from 'nuxt/config'
@@ -197,69 +198,7 @@ export default defineNuxtConfig({
modules: ['@nuxtjs/i18n', '@pinia/nuxt'],
i18n: {
defaultLocale: 'en-US',
- locales: [
- { code: 'af-ZA', name: 'Afrikaans' },
- { code: 'ar-EG', name: 'العربية (مصر)', dir: 'rtl' },
- { code: 'ar-SA', name: 'العربية (السعودية)', dir: 'rtl' },
- { code: 'az-AZ', name: 'Azərbaycan' },
- { code: 'be-BY', name: 'Беларуская' },
- { code: 'bg-BG', name: 'Български' },
- { code: 'bn-BD', name: 'বাংলা' },
- { code: 'ca-ES', name: 'Català' },
- { code: 'ceb-PH', name: 'Cebuano' },
- { code: 'cs-CZ', name: 'Čeština' },
- { code: 'da-DK', name: 'Dansk' },
- { code: 'de-CH', name: 'Deutsch (Schweiz)' },
- { code: 'de-DE', name: 'Deutsch' },
- { code: 'el-GR', name: 'Ελληνικά' },
- { code: 'en-PT', name: 'Pirate English' },
- { code: 'en-UD', name: 'Upside Down' },
- { code: 'en-US', name: 'English (United States)' },
- { code: 'eo-UY', name: 'Esperanto' },
- { code: 'es-419', name: 'Español (Latinoamérica)' },
- { code: 'es-ES', name: 'Español (España)' },
- { code: 'et-EE', name: 'Eesti' },
- { code: 'fa-IR', name: 'فارسی', dir: 'rtl' },
- { code: 'fi-FI', name: 'Suomi' },
- { code: 'fil-PH', name: 'Filipino' },
- { code: 'fr-FR', name: 'Français' },
- { code: 'he-IL', name: 'עברית', dir: 'rtl' },
- { code: 'hi-IN', name: 'हिन्दी' },
- { code: 'hr-HR', name: 'Hrvatski' },
- { code: 'hu-HU', name: 'Magyar' },
- { code: 'id-ID', name: 'Bahasa Indonesia' },
- { code: 'is-IS', name: 'Íslenska' },
- { code: 'it-IT', name: 'Italiano' },
- { code: 'ja-JP', name: '日本語' },
- { code: 'kk-KZ', name: 'Қазақша' },
- { code: 'ko-KR', name: '한국어' },
- { code: 'ky-KG', name: 'Кыргызча' },
- { code: 'lol-US', name: 'LOLCAT' },
- { code: 'lt-LT', name: 'Lietuvių' },
- { code: 'lv-LV', name: 'Latviešu' },
- { code: 'ms-Arab', name: 'بهاس ملايو (جاوي)', dir: 'rtl' },
- { code: 'ms-MY', name: 'Bahasa Melayu' },
- { code: 'nl-NL', name: 'Nederlands' },
- { code: 'no-NO', name: 'Norsk' },
- { code: 'pl-PL', name: 'Polski' },
- { code: 'pt-BR', name: 'Português (Brasil)' },
- { code: 'pt-PT', name: 'Português (Portugal)' },
- { code: 'ro-RO', name: 'Română' },
- { code: 'ru-RU', name: 'Русский' },
- { code: 'sk-SK', name: 'Slovenčina' },
- { code: 'sl-SI', name: 'Slovenščina' },
- { code: 'sr-CS', name: 'Српски (ћирилица)' },
- { code: 'sr-SP', name: 'Srpski (latinica)' },
- { code: 'sv-SE', name: 'Svenska' },
- { code: 'th-TH', name: 'ไทย' },
- { code: 'tl-PH', name: 'Tagalog' },
- { code: 'tr-TR', name: 'Türkçe' },
- { code: 'tt-RU', name: 'Татарча' },
- { code: 'uk-UA', name: 'Українська' },
- { code: 'vi-VN', name: 'Tiếng Việt' },
- { code: 'zh-CN', name: '简体中文' },
- { code: 'zh-TW', name: '繁體中文' },
- ],
+ locales: LOCALES,
strategy: 'no_prefix',
detectBrowserLanguage: {
useCookie: true,
diff --git a/apps/frontend/src/components/ui/IntlFormatted.vue b/apps/frontend/src/components/ui/IntlFormatted.vue
deleted file mode 100644
index 9497be455b..0000000000
--- a/apps/frontend/src/components/ui/IntlFormatted.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
-
- {{ part }}
-
-
diff --git a/apps/frontend/src/i18n.config.ts b/apps/frontend/src/i18n.config.ts
index f7c95dbbd8..71f5943421 100644
--- a/apps/frontend/src/i18n.config.ts
+++ b/apps/frontend/src/i18n.config.ts
@@ -1,59 +1,15 @@
-import IntlMessageFormat from 'intl-messageformat'
-import type { CompileError, MessageCompiler, MessageContext } from 'vue-i18n'
+import { buildLocaleMessages, createMessageCompiler, type CrowdinMessages } from '@modrinth/ui'
-import enUSMessages from './locales/en-US/index.json'
-
-type CrowdinMessages = Record
-
-function transformCrowdinMessages(messages: CrowdinMessages): Record {
- const result: Record = {}
- for (const [key, value] of Object.entries(messages)) {
- if (typeof value === 'string') {
- result[key] = value
- } else if (typeof value === 'object' && value !== null && 'message' in value) {
- result[key] = value.message
- }
- }
- return result
-}
-
-const messageCompiler: MessageCompiler = (msg, { locale, key, onError }) => {
- let messageString: string
-
- if (typeof msg === 'string') {
- messageString = msg
- } else if (typeof msg === 'object' && msg !== null && 'message' in msg) {
- messageString = (msg as { message: string }).message
- } else {
- onError?.(new Error('Invalid message format') as CompileError)
- return () => key
- }
-
- try {
- const formatter = new IntlMessageFormat(messageString, locale)
- return (ctx: MessageContext) => {
- try {
- return formatter.format(ctx.values as Record) as string
- } catch {
- return messageString
- }
- }
- } catch (e) {
- onError?.(e as CompileError)
- return () => key
- }
-}
+const localeModules = import.meta.glob<{ default: CrowdinMessages }>('./locales/*/index.json', {
+ eager: true,
+})
export default defineI18nConfig(() => ({
legacy: false,
locale: 'en-US',
fallbackLocale: 'en-US',
- messageCompiler,
+ messageCompiler: createMessageCompiler(),
missingWarn: false,
fallbackWarn: false,
- messages: {
- 'en-US': transformCrowdinMessages(enUSMessages as CrowdinMessages),
- },
+ messages: buildLocaleMessages(localeModules),
}))
-
-export { transformCrowdinMessages }
diff --git a/apps/frontend/src/locales/en-US/index.json b/apps/frontend/src/locales/en-US/index.json
index 04e03a5099..cf44fd27db 100644
--- a/apps/frontend/src/locales/en-US/index.json
+++ b/apps/frontend/src/locales/en-US/index.json
@@ -2813,18 +2813,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 +2825,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..."
},
diff --git a/apps/frontend/src/plugins/i18n-bridge.ts b/apps/frontend/src/plugins/i18n-bridge.ts
deleted file mode 100644
index fba1c35e3a..0000000000
--- a/apps/frontend/src/plugins/i18n-bridge.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import type { I18nContext } from '@modrinth/ui/src/utils/i18n'
-import { computed, isRef, ref } from 'vue'
-
-export default defineNuxtPlugin({
- name: 'i18n-bridge',
- setup(nuxtApp) {
- nuxtApp.hook('app:created', (vueApp) => {
- const i18n = vueApp.config.globalProperties.$i18n
- if (!i18n) return
-
- const localeRef = isRef(i18n.locale) ? i18n.locale : ref(i18n.locale ?? 'en-US')
- const messagesRef = isRef(i18n.messages) ? i18n.messages : ref(i18n.messages ?? {})
-
- const flatMessages = computed(() => {
- const locale = localeRef.value
- const allMessages = messagesRef.value
- const localeMessages = allMessages[locale] as Record | undefined
- if (!localeMessages) return {}
-
- const result: Record = {}
- for (const [key, value] of Object.entries(localeMessages)) {
- if (typeof value === 'string') {
- result[key] = value
- }
- }
- return result
- })
-
- const i18nContext: I18nContext = {
- locale: localeRef,
- t: (key: string, values?: Record) => {
- try {
- return i18n.t(key, values ?? {})
- } catch {
- return key
- }
- },
- messages: flatMessages,
- }
-
- vueApp.provide('i18n-context', i18nContext)
- })
- },
-})
diff --git a/apps/frontend/src/plugins/locale-loader.ts b/apps/frontend/src/plugins/locale-loader.ts
deleted file mode 100644
index 3b8905d050..0000000000
--- a/apps/frontend/src/plugins/locale-loader.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { transformCrowdinMessages } from '../i18n.config'
-
-type CrowdinMessages = Record
-
-const localeModules = import.meta.glob<{ default: CrowdinMessages }>('../locales/*/index.json', {
- eager: false,
-})
-
-export default defineNuxtPlugin({
- name: 'locale-loader',
- async setup(nuxtApp) {
- const i18n = nuxtApp.$i18n
-
- async function loadLocaleMessages(locale: string): Promise {
- if (locale === 'en-US') return
-
- const path = `../locales/${locale}/index.json`
- const loader = localeModules[path]
-
- if (!loader) {
- console.warn(`Locale file not found for: ${locale}`)
- return
- }
-
- try {
- const messages = await loader()
- const transformed = transformCrowdinMessages(messages.default)
- i18n.setLocaleMessage(locale, transformed)
- } catch (error) {
- console.error(`Failed to load locale: ${locale}`, error)
- }
- }
-
- const currentLocale = i18n.locale.value
- if (currentLocale !== 'en-US') {
- const messages = i18n.getLocaleMessage(currentLocale)
- if (!messages || Object.keys(messages).length === 0) {
- await loadLocaleMessages(currentLocale)
- }
- }
-
- nuxtApp.hook('i18n:beforeLocaleSwitch', async ({ newLocale }) => {
- const messages = i18n.getLocaleMessage(newLocale)
- if (!messages || Object.keys(messages).length === 0) {
- await loadLocaleMessages(newLocale)
- }
- })
- },
-})
diff --git a/apps/frontend/src/utils/i18n-vintl.ts b/apps/frontend/src/utils/i18n-vintl.ts
deleted file mode 100644
index 9dfc45fd98..0000000000
--- a/apps/frontend/src/utils/i18n-vintl.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import IntlMessageFormat from 'intl-messageformat'
-import type { VNode } from 'vue'
-
-export interface MessageDescriptor {
- id: string
- defaultMessage?: string
- description?: string
-}
-
-export type MessageDescriptorMap = Record
-
-export function defineMessage(descriptor: T): T {
- return descriptor
-}
-
-export function defineMessages>(
- descriptors: T,
-): T {
- return descriptors
-}
-
-export interface VIntlFormatters {
- formatMessage(descriptor: MessageDescriptor, values?: Record): string
-}
-
-export function useVIntl(): VIntlFormatters & { locale: globalThis.Ref } {
- const { t, locale } = useI18n()
-
- function formatMessage(descriptor: MessageDescriptor, values?: Record): string {
- const key = descriptor.id
- const translation = t(key, values ?? {}) as string
-
- if (translation && translation !== key) {
- return translation
- }
-
- const defaultMsg = descriptor.defaultMessage ?? key
- try {
- const formatter = new IntlMessageFormat(defaultMsg, locale.value)
- return formatter.format(values ?? {}) as string
- } catch {
- return defaultMsg
- }
- }
-
- return { formatMessage, locale }
-}
-
-export interface IntlFormattedSlotProps {
- children: () => VNode[]
-}
-
-export type IntlFormattedSlots = Record VNode[]>
diff --git a/packages/ui/package.json b/packages/ui/package.json
index ca32918272..6b666e2b94 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -33,6 +33,7 @@
"dependencies": {
"@codemirror/commands": "^6.3.2",
"intl-messageformat": "^10.7.7",
+ "vue-i18n": "^9.14.4",
"@codemirror/lang-markdown": "^6.2.3",
"@codemirror/language": "^6.9.3",
"@codemirror/state": "^6.3.2",
diff --git a/packages/ui/src/utils/i18n.ts b/packages/ui/src/utils/i18n.ts
index cf361dc6fb..bdf1ca6d97 100644
--- a/packages/ui/src/utils/i18n.ts
+++ b/packages/ui/src/utils/i18n.ts
@@ -1,5 +1,7 @@
import IntlMessageFormat from 'intl-messageformat'
-import { inject, type InjectionKey, provide, type Ref, ref } from 'vue'
+import type { Ref } from 'vue'
+import type { CompileError, MessageCompiler, MessageContext } from 'vue-i18n'
+import { useI18n } from 'vue-i18n'
export interface MessageDescriptor {
id: string
@@ -9,6 +11,8 @@ export interface MessageDescriptor {
export type MessageDescriptorMap = Record
+export type CrowdinMessages = Record
+
export function defineMessage(descriptor: T): T {
return descriptor
}
@@ -19,56 +23,165 @@ export function defineMessages
- t: (key: string, values?: Record) => string
- messages: Ref>
+export interface LocaleDefinition {
+ code: string
+ name: string
+ dir?: 'ltr' | 'rtl'
}
-const i18nKey: InjectionKey = Symbol('i18n')
-
-export function provideI18n(context: I18nContext) {
- provide(i18nKey, context)
+export const LOCALES: LocaleDefinition[] = [
+ { code: 'af-ZA', name: 'Afrikaans' },
+ { code: 'ar-EG', name: 'العربية (مصر)', dir: 'rtl' },
+ { code: 'ar-SA', name: 'العربية (السعودية)', dir: 'rtl' },
+ { code: 'az-AZ', name: 'Azərbaycan' },
+ { code: 'be-BY', name: 'Беларуская' },
+ { code: 'bg-BG', name: 'Български' },
+ { code: 'bn-BD', name: 'বাংলা' },
+ { code: 'ca-ES', name: 'Català' },
+ { code: 'ceb-PH', name: 'Cebuano' },
+ { code: 'cs-CZ', name: 'Čeština' },
+ { code: 'da-DK', name: 'Dansk' },
+ { code: 'de-CH', name: 'Deutsch (Schweiz)' },
+ { code: 'de-DE', name: 'Deutsch' },
+ { code: 'el-GR', name: 'Ελληνικά' },
+ { code: 'en-PT', name: 'Pirate English' },
+ { code: 'en-UD', name: 'Upside Down' },
+ { code: 'en-US', name: 'English (United States)' },
+ { code: 'eo-UY', name: 'Esperanto' },
+ { code: 'es-419', name: 'Español (Latinoamérica)' },
+ { code: 'es-ES', name: 'Español (España)' },
+ { code: 'et-EE', name: 'Eesti' },
+ { code: 'fa-IR', name: 'فارسی', dir: 'rtl' },
+ { code: 'fi-FI', name: 'Suomi' },
+ { code: 'fil-PH', name: 'Filipino' },
+ { code: 'fr-FR', name: 'Français' },
+ { code: 'he-IL', name: 'עברית', dir: 'rtl' },
+ { code: 'hi-IN', name: 'हिन्दी' },
+ { code: 'hr-HR', name: 'Hrvatski' },
+ { code: 'hu-HU', name: 'Magyar' },
+ { code: 'id-ID', name: 'Bahasa Indonesia' },
+ { code: 'is-IS', name: 'Íslenska' },
+ { code: 'it-IT', name: 'Italiano' },
+ { code: 'ja-JP', name: '日本語' },
+ { code: 'kk-KZ', name: 'Қазақша' },
+ { code: 'ko-KR', name: '한국어' },
+ { code: 'ky-KG', name: 'Кыргызча' },
+ { code: 'lol-US', name: 'LOLCAT' },
+ { code: 'lt-LT', name: 'Lietuvių' },
+ { code: 'lv-LV', name: 'Latviešu' },
+ { code: 'ms-Arab', name: 'بهاس ملايو (جاوي)', dir: 'rtl' },
+ { code: 'ms-MY', name: 'Bahasa Melayu' },
+ { code: 'nl-NL', name: 'Nederlands' },
+ { code: 'no-NO', name: 'Norsk' },
+ { code: 'pl-PL', name: 'Polski' },
+ { code: 'pt-BR', name: 'Português (Brasil)' },
+ { code: 'pt-PT', name: 'Português (Portugal)' },
+ { code: 'ro-RO', name: 'Română' },
+ { code: 'ru-RU', name: 'Русский' },
+ { code: 'sk-SK', name: 'Slovenčina' },
+ { code: 'sl-SI', name: 'Slovenščina' },
+ { code: 'sr-CS', name: 'Српски (ћирилица)' },
+ { code: 'sr-SP', name: 'Srpski (latinica)' },
+ { code: 'sv-SE', name: 'Svenska' },
+ { code: 'th-TH', name: 'ไทย' },
+ { code: 'tl-PH', name: 'Tagalog' },
+ { code: 'tr-TR', name: 'Türkçe' },
+ { code: 'tt-RU', name: 'Татарча' },
+ { code: 'uk-UA', name: 'Українська' },
+ { code: 'vi-VN', name: 'Tiếng Việt' },
+ { code: 'zh-CN', name: '简体中文' },
+ { code: 'zh-TW', name: '繁體中文' },
+]
+
+export function transformCrowdinMessages(messages: CrowdinMessages): Record {
+ const result: Record = {}
+ for (const [key, value] of Object.entries(messages)) {
+ if (typeof value === 'string') {
+ result[key] = value
+ } else if (typeof value === 'object' && value !== null && 'message' in value) {
+ result[key] = value.message
+ }
+ }
+ return result
}
-export function useI18nContext(): I18nContext {
- const injectedContext = inject(i18nKey)
- if (injectedContext) {
- return injectedContext
+const LOCALE_CODES = new Set(LOCALES.map((l) => l.code))
+
+/**
+ * Builds locale messages from glob-imported modules.
+ * Only includes locales that are defined in the LOCALES array.
+ * Usage: buildLocaleMessages(import.meta.glob('./locales/* /index.json', { eager: true }))
+ */
+export function buildLocaleMessages(
+ modules: Record,
+): Record> {
+ const messages: Record> = {}
+ for (const [path, module] of Object.entries(modules)) {
+ // Extract locale code from path like './locales/en-US/index.json'
+ const match = path.match(/\/([^/]+)\/index\.json$/)
+ if (match) {
+ const locale = match[1]
+ // Only include locales that are in our LOCALES list
+ if (LOCALE_CODES.has(locale)) {
+ messages[locale] = transformCrowdinMessages(module.default)
+ }
+ }
}
+ return messages
+}
- const bridgeContext = inject('i18n-context', null as unknown as I18nContext)
- if (bridgeContext) {
- return bridgeContext
- }
+/**
+ * Creates a vue-i18n message compiler that uses IntlMessageFormat for ICU syntax support.
+ * This enables pluralization, select, and other ICU message features.
+ */
+export function createMessageCompiler(): MessageCompiler {
+ return (msg, { locale, key, onError }) => {
+ let messageString: string
+
+ if (typeof msg === 'string') {
+ messageString = msg
+ } else if (typeof msg === 'object' && msg !== null && 'message' in msg) {
+ messageString = (msg as { message: string }).message
+ } else {
+ onError?.(new Error('Invalid message format') as CompileError)
+ return () => key
+ }
- return {
- locale: ref('en-US'),
- t: (key: string) => key,
- messages: ref({}),
+ try {
+ const formatter = new IntlMessageFormat(messageString, locale)
+ return (ctx: MessageContext) => {
+ try {
+ return formatter.format(ctx.values as Record) as string
+ } catch {
+ return messageString
+ }
+ }
+ } catch (e) {
+ onError?.(e as CompileError)
+ return () => key
+ }
}
}
-
export interface VIntlFormatters {
formatMessage(descriptor: MessageDescriptor, values?: Record): string
}
+/**
+ * Composable that provides formatMessage() with the same API as @vintl/vintl.
+ * Uses vue-i18n's useI18n() under the hood.
+ */
export function useVIntl(): VIntlFormatters & { locale: Ref } {
- const { t, locale, messages } = useI18nContext()
+ const { t, locale } = useI18n()
function formatMessage(descriptor: MessageDescriptor, values?: Record): string {
const key = descriptor.id
- const translation = messages.value[key] ?? t(key, values)
+ const translation = t(key, values ?? {})
if (translation && translation !== key) {
- try {
- const formatter = new IntlMessageFormat(translation, locale.value)
- return formatter.format(values ?? {}) as string
- } catch {
- return translation
- }
+ return translation as string
}
+ // Fallback to defaultMessage if key not found
const defaultMsg = descriptor.defaultMessage ?? key
try {
const formatter = new IntlMessageFormat(defaultMsg, locale.value)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b07e8a3db4..2248d5c2ff 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -628,6 +628,9 @@ importers:
three:
specifier: ^0.172.0
version: 0.172.0
+ vue-i18n:
+ specifier: ^9.14.4
+ version: 9.14.5(vue@3.5.13(typescript@5.5.4))
vue-multiselect:
specifier: 3.0.0
version: 3.0.0
From b6bd1810cb36d48f458c8e505ca7c69893c4af5f Mon Sep 17 00:00:00 2001
From: "Calum H. (IMB11)"
Date: Fri, 26 Dec 2025 18:32:03 +0000
Subject: [PATCH 03/10] fix: broken imports
---
apps/app-frontend/src/App.vue | 3 +-
.../src/components/ui/UpdateToast.vue | 3 +-
.../src/components/ui/friends/FriendsList.vue | 5 +-
.../components/ui/friends/FriendsSection.vue | 10 +-
.../ui/instance_settings/GeneralSettings.vue | 3 +-
.../ui/instance_settings/HooksSettings.vue | 3 +-
.../InstallationSettings.vue | 3 +-
.../ui/instance_settings/JavaSettings.vue | 3 +-
.../ui/instance_settings/WindowSettings.vue | 3 +-
.../components/ui/modal/AppSettingsModal.vue | 3 +-
.../ui/modal/InstanceSettingsModal.vue | 3 +-
.../src/components/ui/world/InstanceItem.vue | 2 +-
.../src/components/ui/world/WorldItem.vue | 5 +-
.../ui/world/modal/AddServerModal.vue | 9 +-
.../ui/world/modal/EditServerModal.vue | 9 +-
.../modal/EditSingleplayerWorldModal.vue | 10 +-
.../ui/world/modal/HideFromHomeOption.vue | 4 +-
.../ui/world/modal/ServerModalBody.vue | 3 +-
apps/app-frontend/src/pages/Browse.vue | 12 +-
apps/app-frontend/src/pages/instance/Mods.vue | 3 +-
.../src/pages/instance/Worlds.vue | 2 +-
.../src/components/ui/ProjectMemberHeader.vue | 9 +-
.../ui/create/CollectionCreateModal.vue | 4 +-
.../components/ui/create/CreateLimitAlert.vue | 4 +-
.../ui/create/OrganizationCreateModal.vue | 4 +-
.../ui/create/ProjectCreateModal.vue | 10 +-
.../ui/dashboard/CreatorTaxFormModal.vue | 5 +-
.../ui/dashboard/CreatorWithdrawModal.vue | 10 +-
.../ui/dashboard/RevenueInputField.vue | 10 +-
.../ui/dashboard/WithdrawFeeBreakdown.vue | 3 +-
.../withdraw-stages/CompletionStage.vue | 4 +-
.../LegacyPaypalDetailsStage.vue | 5 +-
.../withdraw-stages/MethodSelectionStage.vue | 5 +-
.../withdraw-stages/MuralpayDetailsStage.vue | 5 +-
.../withdraw-stages/MuralpayKycStage.vue | 3 +-
.../withdraw-stages/TaxFormStage.vue | 11 +-
.../TremendousDetailsStage.vue | 5 +-
.../ui/moderation/ModerationProjectNags.vue | 4 +-
.../src/components/ui/news/LatestNewsRow.vue | 4 +-
.../servers/marketing/ServerPlanSelector.vue | 3 +-
.../ui/servers/notice/NoticeDashboardItem.vue | 3 +-
apps/frontend/src/error.vue | 5 +-
apps/frontend/src/layouts/default.vue | 2 +-
apps/frontend/src/pages/[type]/[id].vue | 2 +-
.../src/pages/[type]/[id]/settings.vue | 2 +-
.../[type]/[id]/settings/environment.vue | 4 +-
.../pages/[type]/[id]/settings/general.vue | 5 +-
.../src/pages/admin/servers/notices.vue | 2 +-
apps/frontend/src/pages/app.vue | 12 +-
apps/frontend/src/pages/auth/authorize.vue | 2 +-
apps/frontend/src/pages/auth/sign-in.vue | 3 +-
apps/frontend/src/pages/auth/sign-up.vue | 3 +-
apps/frontend/src/pages/auth/welcome.vue | 4 +-
apps/frontend/src/pages/collection/[id].vue | 5 +-
.../src/pages/dashboard/affiliate-links.vue | 3 +-
.../src/pages/dashboard/revenue/index.vue | 2 +-
.../src/pages/dashboard/revenue/transfers.vue | 3 +-
apps/frontend/src/pages/discover.vue | 4 +-
apps/frontend/src/pages/hosting/index.vue | 2 +-
.../src/pages/hosting/manage/[id].vue | 2 +-
apps/frontend/src/pages/index.vue | 12 +-
apps/frontend/src/pages/moderation.vue | 3 +-
apps/frontend/src/pages/moderation/index.vue | 11 +-
.../src/pages/moderation/reports/index.vue | 10 +-
.../src/pages/moderation/technical-review.vue | 3 +-
apps/frontend/src/pages/report.vue | 6 +-
apps/frontend/src/pages/settings/index.vue | 12 +-
apps/frontend/src/pages/settings/language.vue | 3 +-
apps/frontend/src/pages/settings/pats.vue | 2 +-
apps/frontend/src/pages/settings/profile.vue | 11 +-
apps/frontend/src/pages/user/[id].vue | 2 +-
.../src/providers/creator-withdraw.ts | 2 +-
apps/frontend/src/utils/muralpay-rails.ts | 2 +-
scripts/fix-i18n-imports.ts | 120 ++++++++++++++++++
74 files changed, 336 insertions(+), 142 deletions(-)
create mode 100644 scripts/fix-i18n-imports.ts
diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue
index 4a56f35eed..4901748de8 100644
--- a/apps/app-frontend/src/App.vue
+++ b/apps/app-frontend/src/App.vue
@@ -31,6 +31,7 @@ import {
Button,
ButtonStyled,
commonMessages,
+ defineMessages,
NewsArticleCard,
NotificationPanel,
OverflowMenu,
@@ -39,6 +40,7 @@ import {
provideNotificationManager,
providePageContext,
useDebugLogger,
+ useVIntl,
} from '@modrinth/ui'
import { renderString } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
@@ -97,7 +99,6 @@ import {
import { useError } from '@/store/error.js'
import { useInstall } from '@/store/install.js'
import { useLoading, useTheming } from '@/store/state'
-import { defineMessages, useVIntl } from '@/utils/i18n-vintl'
import { create_profile_and_install_from_file } from './helpers/pack'
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
diff --git a/apps/app-frontend/src/components/ui/UpdateToast.vue b/apps/app-frontend/src/components/ui/UpdateToast.vue
index 95e4f5957b..b9b95f235a 100644
--- a/apps/app-frontend/src/components/ui/UpdateToast.vue
+++ b/apps/app-frontend/src/components/ui/UpdateToast.vue
@@ -1,11 +1,10 @@