Make settings page localizable (#5294)

* make settings localizable

* move plan names to common messages

* unknown -> plan-unknown

* prepr:frontend
This commit is contained in:
xinyihl
2026-03-19 00:16:04 +08:00
committed by GitHub
parent 61754efca4
commit cf1b5f5e2d
12 changed files with 1420 additions and 271 deletions

View File

@@ -1,6 +1,13 @@
<script setup lang="ts">
import { type MessageDescriptor, useFormatPrice } from '@modrinth/ui'
import { ButtonStyled, defineMessage, defineMessages, ServersSpecs, useVIntl } from '@modrinth/ui'
import {
ButtonStyled,
commonMessages,
defineMessage,
defineMessages,
ServersSpecs,
useVIntl,
} from '@modrinth/ui'
const { formatMessage } = useVIntl()
const formatPrice = useFormatPrice()
@@ -37,10 +44,7 @@ const plans: Record<
buttonColor: 'blue',
accentText: 'text-blue',
accentBg: 'bg-bg-blue',
name: defineMessage({
id: 'servers.plan.small.name',
defaultMessage: 'Small',
}),
name: commonMessages.planSmallLabel,
description: defineMessage({
id: 'servers.plan.small.description',
defaultMessage: 'Perfect for 15 friends with a few light mods.',
@@ -51,10 +55,7 @@ const plans: Record<
buttonColor: 'green',
accentText: 'text-green',
accentBg: 'bg-bg-green',
name: defineMessage({
id: 'servers.plan.medium.name',
defaultMessage: 'Medium',
}),
name: commonMessages.planMediumLabel,
description: defineMessage({
id: 'servers.plan.medium.description',
defaultMessage: 'Great for 615 players and multiple mods.',
@@ -65,10 +66,7 @@ const plans: Record<
buttonColor: 'purple',
accentText: 'text-purple',
accentBg: 'bg-bg-purple',
name: defineMessage({
id: 'servers.plan.large.name',
defaultMessage: 'Large',
}),
name: commonMessages.planLargeLabel,
description: defineMessage({
id: 'servers.plan.large.description',
defaultMessage: 'Ideal for 1525 players, modpacks, or heavy modding.',

View File

@@ -2855,20 +2855,209 @@
"servers.plan.large.description": {
"message": "Ideal for 1525 players, modpacks, or heavy modding."
},
"servers.plan.large.name": {
"message": "Large"
},
"servers.plan.medium.description": {
"message": "Great for 615 players and multiple mods."
},
"servers.plan.medium.name": {
"message": "Medium"
},
"servers.plan.small.description": {
"message": "Perfect for 15 friends with a few light mods."
},
"servers.plan.small.name": {
"message": "Small"
"settings.account.button.complete-setup": {
"message": "Complete setup"
},
"settings.account.data-export.action.download": {
"message": "Download export"
},
"settings.account.data-export.action.generate": {
"message": "Generate export"
},
"settings.account.data-export.action.generating": {
"message": "Generating export..."
},
"settings.account.data-export.description": {
"message": "Request a copy of all your personal data you have uploaded to Modrinth. This may take several minutes to complete."
},
"settings.account.data-export.title": {
"message": "Data export"
},
"settings.account.delete.confirm.description": {
"message": "This will **immediately delete all of your user data and follows**. This will not delete your projects. Deleting your account cannot be reversed.<br><br>If you need help with your account, get support on the [Modrinth Discord](https://discord.modrinth.com)."
},
"settings.account.delete.confirm.proceed": {
"message": "Delete this account"
},
"settings.account.delete.confirm.title": {
"message": "Are you sure you want to delete your account?"
},
"settings.account.delete.section.action": {
"message": "Delete account"
},
"settings.account.delete.section.description": {
"message": "Once you delete your account, there is no going back. Deleting your account will remove all attached data, excluding projects, from our servers."
},
"settings.account.delete.section.title": {
"message": "Delete account"
},
"settings.account.email.action.save": {
"message": "Save email"
},
"settings.account.email.field.label": {
"message": "Email address"
},
"settings.account.email.field.placeholder": {
"message": "Enter your email address..."
},
"settings.account.email.modal.header.add": {
"message": "Add email"
},
"settings.account.email.modal.header.change": {
"message": "Change email"
},
"settings.account.email.modal.notice": {
"message": "Your account information is not displayed publicly."
},
"settings.account.password.action.remove": {
"message": "Remove password"
},
"settings.account.password.action.save": {
"message": "Save password"
},
"settings.account.password.error.mismatch": {
"message": "Input passwords do not match!"
},
"settings.account.password.field.confirm-current.description": {
"message": "Please enter your password to proceed."
},
"settings.account.password.field.confirm-current.label": {
"message": "Confirm password"
},
"settings.account.password.field.confirm-current.placeholder": {
"message": "Confirm password"
},
"settings.account.password.field.confirm-new.label": {
"message": "Confirm new password"
},
"settings.account.password.field.confirm-new.placeholder": {
"message": "Confirm new password"
},
"settings.account.password.field.new.label": {
"message": "New password"
},
"settings.account.password.field.new.placeholder": {
"message": "New password"
},
"settings.account.password.field.old.label": {
"message": "Old password"
},
"settings.account.password.field.old.placeholder": {
"message": "Old password"
},
"settings.account.password.modal.header.add": {
"message": "Add password"
},
"settings.account.password.modal.header.change": {
"message": "Change password"
},
"settings.account.password.modal.header.remove": {
"message": "Remove password"
},
"settings.account.providers.action.add": {
"message": "Add"
},
"settings.account.providers.modal.header": {
"message": "Authentication providers"
},
"settings.account.providers.table.actions": {
"message": "Actions"
},
"settings.account.providers.table.provider": {
"message": "Provider"
},
"settings.account.security.email.action.add": {
"message": "Add email"
},
"settings.account.security.email.action.change": {
"message": "Change email"
},
"settings.account.security.email.description": {
"message": "Changes the email associated with your account."
},
"settings.account.security.email.title": {
"message": "Email"
},
"settings.account.security.password.action.add": {
"message": "Add password"
},
"settings.account.security.password.action.change": {
"message": "Change password"
},
"settings.account.security.password.description.change": {
"message": "Change the password used to login to your account."
},
"settings.account.security.password.description.change-or-remove": {
"message": "Change or remove the password used to login to your account."
},
"settings.account.security.password.description.set": {
"message": "Set a permanent password to login to your account."
},
"settings.account.security.password.title": {
"message": "Password"
},
"settings.account.security.providers.action.manage": {
"message": "Manage providers"
},
"settings.account.security.providers.description": {
"message": "Add or remove sign-on methods from your account, including GitHub, GitLab, Microsoft, Discord, Steam, and Google."
},
"settings.account.security.providers.title": {
"message": "Manage authentication providers"
},
"settings.account.security.title": {
"message": "Account security"
},
"settings.account.security.two-factor.action.remove": {
"message": "Remove 2FA"
},
"settings.account.security.two-factor.action.setup": {
"message": "Setup 2FA"
},
"settings.account.security.two-factor.description": {
"message": "Add an additional layer of security to your account during login."
},
"settings.account.security.two-factor.title": {
"message": "Two-factor authentication"
},
"settings.account.two-factor.backup.intro": {
"message": "Download and save these back-up codes in a safe place. You can use these in-place of a 2FA code if you ever lose access to your device! You should protect these codes like your password."
},
"settings.account.two-factor.backup.single-use": {
"message": "Backup codes can only be used once."
},
"settings.account.two-factor.error.incorrect-code": {
"message": "The code entered is incorrect!"
},
"settings.account.two-factor.field.code.description": {
"message": "Please enter a two-factor code to proceed."
},
"settings.account.two-factor.field.code.label": {
"message": "Enter two-factor code"
},
"settings.account.two-factor.field.code.placeholder": {
"message": "Enter code..."
},
"settings.account.two-factor.setup.intro": {
"message": "Two-factor authentication keeps your account secure by requiring access to a second device in order to sign in."
},
"settings.account.two-factor.setup.manual-secret": {
"message": "If the QR code does not scan, you can manually enter the secret:"
},
"settings.account.two-factor.setup.scan": {
"message": "Scan the QR code with <authy-link>Authy</authy-link>, <microsoft-authenticator-link>Microsoft Authenticator</microsoft-authenticator-link>, or any other 2FA app to begin."
},
"settings.account.two-factor.verify.description": {
"message": "Enter the one-time code from authenticator to verify access."
},
"settings.account.two-factor.verify.label": {
"message": "Verify code"
},
"settings.applications.about": {
"message": "About"
@@ -2951,12 +3140,12 @@
"settings.applications.field.url.placeholder": {
"message": "https://example.com"
},
"settings.applications.head-title": {
"message": "Applications"
},
"settings.applications.modal.header": {
"message": "Application information"
},
"settings.applications.notification.error.title": {
"message": "An error occurred"
},
"settings.applications.notification.icon-updated.description": {
"message": "Your application icon has been updated."
},
@@ -2966,6 +3155,90 @@
"settings.applications.secret.disclaimer": {
"message": "Save your secret now, it will be hidden after you leave this page!"
},
"settings.authorizations.about-this-app": {
"message": "About this app"
},
"settings.authorizations.by": {
"message": "by"
},
"settings.authorizations.description": {
"message": "When you authorize an application with your Modrinth account, you grant it access to your account. You can manage and review access to your account here at any time."
},
"settings.authorizations.empty-state": {
"message": "We currently can't display your authorized apps, we're working to fix this. Please visit this page at a later date!"
},
"settings.authorizations.head-title": {
"message": "Authorizations"
},
"settings.authorizations.revoke.action": {
"message": "Revoke"
},
"settings.authorizations.revoke.confirm.description": {
"message": "This will revoke the application's access to your account. You can always re-authorize it later."
},
"settings.authorizations.revoke.confirm.title": {
"message": "Are you sure you want to revoke this application?"
},
"settings.billing.charges.description": {
"message": "All of your past charges to your Modrinth account will be listed here:"
},
"settings.billing.charges.product.medal-trial": {
"message": "Medal Server Trial"
},
"settings.billing.charges.product.midas": {
"message": "Modrinth Plus"
},
"settings.billing.charges.product.pyro": {
"message": "Modrinth Hosting"
},
"settings.billing.expires": {
"message": "Expires {date}"
},
"settings.billing.interval.month": {
"message": "month"
},
"settings.billing.interval.monthly": {
"message": "monthly"
},
"settings.billing.interval.year": {
"message": "year"
},
"settings.billing.interval.yearly": {
"message": "yearly"
},
"settings.billing.midas.benefits.ad-free": {
"message": "Ad-free browsing on modrinth.com and Modrinth App"
},
"settings.billing.midas.benefits.badge": {
"message": "Modrinth+ badge on your profile"
},
"settings.billing.midas.benefits.support": {
"message": "Support Modrinth and creators directly"
},
"settings.billing.midas.benefits.title": {
"message": "Benefits"
},
"settings.billing.midas.save-per-year": {
"message": "Save {amount}/year by switching to yearly billing!"
},
"settings.billing.midas.status.cancelled.line1": {
"message": "You've cancelled your subscription."
},
"settings.billing.midas.status.cancelled.line2": {
"message": "You will retain your perks until the end of the current billing cycle."
},
"settings.billing.midas.status.failed": {
"message": "Your subscription payment failed. Please update your payment method."
},
"settings.billing.midas.status.open": {
"message": "You're currently subscribed to:"
},
"settings.billing.midas.status.processing": {
"message": "Your payment is being processed. Perks will activate once payment is complete."
},
"settings.billing.midas.upsell": {
"message": "Become a subscriber to Modrinth Plus!"
},
"settings.billing.modal.cancel.action": {
"message": "Cancel subscription"
},
@@ -2984,6 +3257,12 @@
"settings.billing.modal.delete.title": {
"message": "Are you sure you want to remove this payment method?"
},
"settings.billing.next": {
"message": "Next:"
},
"settings.billing.or-yearly-save": {
"message": "Or {price} / year (save {percent}%)!"
},
"settings.billing.payment_method.action.add": {
"message": "Add payment method"
},
@@ -3005,18 +3284,99 @@
"settings.billing.payment_method.title": {
"message": "Payment methods"
},
"settings.billing.plan.title": {
"message": "{size} Plan"
},
"settings.billing.price.per-interval": {
"message": "{price} / {interval}"
},
"settings.billing.price.slash-interval": {
"message": "/{interval}"
},
"settings.billing.pyro.cpu": {
"message": "{shared} Shared CPUs (Bursts up to {bursts} CPUs)"
},
"settings.billing.pyro.linked-server.not-found": {
"message": "A linked server couldn't be found for this subscription. There are a few possible explanations for this. If you just purchased your server, this is normal. It could take up to an hour for your server to be provisioned. Otherwise, if you purchased this server a while ago, it has likely since been suspended. If this is not what you were expecting, please contact Modrinth Support with the following information:"
},
"settings.billing.pyro.linked-server.server-id": {
"message": "Server ID: {id}"
},
"settings.billing.pyro.linked-server.stripe-id": {
"message": "Stripe ID: {id}"
},
"settings.billing.pyro.ram": {
"message": "{gb} GB RAM"
},
"settings.billing.pyro.resubscribe.error.text": {
"message": "An error occurred while resubscribing to your Modrinth server."
},
"settings.billing.pyro.resubscribe.error.title": {
"message": "Error resubscribing"
},
"settings.billing.pyro.resubscribe.request-submitted.text": {
"message": "If the server is currently suspended, it may take up to 10 minutes for another charge attempt to be made."
},
"settings.billing.pyro.resubscribe.request-submitted.title": {
"message": "Resubscription request submitted"
},
"settings.billing.pyro.resubscribe.success.text": {
"message": "Server subscription resubscribed successfully"
},
"settings.billing.pyro.status.failed": {
"message": "Your subscription payment failed. Please update your payment method, then resubscribe."
},
"settings.billing.pyro.status.processing": {
"message": "Your payment is being processed. Your server will activate once payment is complete."
},
"settings.billing.pyro.storage": {
"message": "{gb} GB SSD"
},
"settings.billing.pyro.swap": {
"message": "{gb} GB Swap"
},
"settings.billing.pyro_subscription.description": {
"message": "Manage your Modrinth Server subscriptions."
},
"settings.billing.pyro_subscription.title": {
"message": "Modrinth Server Subscriptions"
},
"settings.billing.renews": {
"message": "Renews {date}"
},
"settings.billing.resubscribe": {
"message": "Resubscribe"
},
"settings.billing.since": {
"message": "Since {date}"
},
"settings.billing.subscribe": {
"message": "Subscribe"
},
"settings.billing.subscription.description": {
"message": "Manage your Modrinth subscriptions."
},
"settings.billing.subscription.title": {
"message": "Subscriptions"
},
"settings.billing.switch.switching-to-interval": {
"message": "Switching to {interval}"
},
"settings.billing.switch.to-interval": {
"message": "Switch to {interval}"
},
"settings.billing.switch.tooltip.monthly-additional-per-year": {
"message": "Monthly billing will cost you an additional {amount} per year"
},
"settings.billing.switches-to-billing-on": {
"message": "Switches to {interval} billing on {date}"
},
"settings.billing.update-method": {
"message": "Update method"
},
"settings.billing.upgrade": {
"message": "Upgrade"
},
"settings.display.banner.developer-mode.button": {
"message": "Deactivate developer mode"
},
@@ -3029,6 +3389,12 @@
"settings.display.flags.title": {
"message": "Toggle features"
},
"settings.display.notification.developer-mode-deactivated.text": {
"message": "Developer mode has been disabled"
},
"settings.display.notification.developer-mode-deactivated.title": {
"message": "Developer mode deactivated"
},
"settings.display.project-list-layouts.datapack": {
"message": "Data Packs page"
},
@@ -3038,6 +3404,15 @@
"settings.display.project-list-layouts.mod": {
"message": "Mods page"
},
"settings.display.project-list-layouts.mode.gallery": {
"message": "Gallery"
},
"settings.display.project-list-layouts.mode.grid": {
"message": "Grid"
},
"settings.display.project-list-layouts.mode.rows": {
"message": "Rows"
},
"settings.display.project-list-layouts.modpack": {
"message": "Modpacks page"
},
@@ -3098,6 +3473,9 @@
"settings.display.theme.title": {
"message": "Color theme"
},
"settings.head-title": {
"message": "Display settings"
},
"settings.pats.action.create": {
"message": "Create a PAT"
},
@@ -3158,6 +3536,9 @@
"settings.profile.description": {
"message": "Your profile information is publicly viewable on Modrinth and through the <docs-link>Modrinth API</docs-link>."
},
"settings.profile.head-title": {
"message": "Profile settings"
},
"settings.profile.profile-info": {
"message": "Profile information"
},
@@ -3188,6 +3569,15 @@
"settings.sessions.unknown-platform": {
"message": "Unknown platform"
},
"settings.sidebar.label.account": {
"message": "Account"
},
"settings.sidebar.label.developer": {
"message": "Developer"
},
"settings.sidebar.label.display": {
"message": "Display"
},
"ui.latest-news-row.latest-news": {
"message": "Latest news from Modrinth"
},

View File

@@ -8,7 +8,7 @@
<NavStack
:items="
[
{ type: 'heading', label: 'Display' },
{ type: 'heading', label: formatMessage(messages.display) },
{
link: '/settings',
label: formatMessage(commonSettingsMessages.appearance),
@@ -20,7 +20,7 @@
icon: LanguagesIcon,
badge: `${formatMessage(commonMessages.beta)}`,
},
auth.user ? { type: 'heading', label: 'Account' } : null,
auth.user ? { type: 'heading', label: formatMessage(messages.account) } : null,
auth.user
? {
link: '/settings/profile',
@@ -56,7 +56,7 @@
icon: CardIcon,
}
: null,
auth.user ? { type: 'heading', label: 'Developer' } : null,
auth.user ? { type: 'heading', label: formatMessage(messages.developer) } : null,
auth.user
? {
link: '/settings/pats',
@@ -93,12 +93,27 @@ import {
ShieldIcon,
UserIcon,
} from '@modrinth/assets'
import { commonMessages, commonSettingsMessages, useVIntl } from '@modrinth/ui'
import { commonMessages, commonSettingsMessages, defineMessages, useVIntl } from '@modrinth/ui'
import NavStack from '~/components/ui/NavStack.vue'
const { formatMessage } = useVIntl()
const messages = defineMessages({
display: {
id: 'settings.sidebar.label.display',
defaultMessage: 'Display',
},
account: {
id: 'settings.sidebar.label.account',
defaultMessage: 'Account',
},
developer: {
id: 'settings.sidebar.label.developer',
defaultMessage: 'Developer',
},
})
const route = useNativeRoute()
const auth = await useAuth()

View File

@@ -2,29 +2,34 @@
<div>
<ConfirmModal
ref="modal_confirm"
title="Are you sure you want to delete your account?"
description="This will **immediately delete all of your user data and follows**. This will not delete your projects. Deleting your account cannot be reversed.<br><br>If you need help with your account, get support on the [Modrinth Discord](https://discord.modrinth.com)."
proceed-label="Delete this account"
:title="formatMessage(messages.deleteAccountConfirmTitle)"
:description="formatMessage(messages.deleteAccountConfirmDescription)"
:proceed-label="formatMessage(messages.deleteAccountConfirmProceed)"
:confirmation-text="auth.user.username"
:has-to-type="true"
@proceed="deleteAccount"
/>
<Modal ref="changeEmailModal" :header="`${auth.user.email ? 'Change' : 'Add'} email`">
<Modal
ref="changeEmailModal"
:header="`${auth.user.email ? formatMessage(messages.changeEmailHeaderChange) : formatMessage(messages.changeEmailHeaderAdd)}`"
>
<div class="universal-modal">
<p>Your account information is not displayed publicly.</p>
<label for="email-input"><span class="label__title">Email address</span> </label>
<p>{{ formatMessage(messages.emailNotPublicNotice) }}</p>
<label for="email-input">
<span class="label__title">{{ formatMessage(messages.emailAddressLabel) }}</span>
</label>
<StyledInput
id="email-input"
v-model="email"
:maxlength="2048"
type="email"
:placeholder="`Enter your email address...`"
:placeholder="formatMessage(messages.emailAddressPlaceholder)"
@keyup.enter="saveEmail()"
/>
<div class="input-group push-right">
<button class="iconified-button" @click="$refs.changeEmailModal.hide()">
<XIcon />
Cancel
{{ formatMessage(commonMessages.cancelButton) }}
</button>
<button
type="button"
@@ -33,7 +38,7 @@
@click="saveEmail()"
>
<SaveIcon />
Save email
{{ formatMessage(messages.saveEmailButton) }}
</button>
</div>
</div>
@@ -41,22 +46,28 @@
<Modal
ref="managePasswordModal"
:header="`${
removePasswordMode ? 'Remove' : auth.user.has_password ? 'Change' : 'Add'
} password`"
removePasswordMode
? formatMessage(messages.passwordHeaderRemove)
: auth.user.has_password
? formatMessage(messages.passwordHeaderChange)
: formatMessage(messages.passwordHeaderAdd)
}`"
>
<div class="universal-modal">
<ul
v-if="newPassword !== confirmNewPassword && confirmNewPassword.length > 0"
class="known-errors"
>
<li>Input passwords do not match!</li>
<li>{{ formatMessage(messages.passwordsDoNotMatchError) }}</li>
</ul>
<label v-if="removePasswordMode" for="old-password">
<span class="label__title">Confirm password</span>
<span class="label__description">Please enter your password to proceed.</span>
<span class="label__title">{{ formatMessage(messages.confirmPasswordLabel) }}</span>
<span class="label__description">{{
formatMessage(messages.confirmPasswordDescription)
}}</span>
</label>
<label v-else-if="auth.user.has_password" for="old-password">
<span class="label__title">Old password</span>
<span class="label__title">{{ formatMessage(messages.oldPasswordLabel) }}</span>
</label>
<StyledInput
v-if="auth.user.has_password"
@@ -65,35 +76,41 @@
:maxlength="2048"
type="password"
autocomplete="current-password"
:placeholder="`${removePasswordMode ? 'Confirm' : 'Old'} password`"
:placeholder="
removePasswordMode
? formatMessage(messages.confirmPasswordPlaceholder)
: formatMessage(messages.oldPasswordPlaceholder)
"
/>
<template v-if="!removePasswordMode">
<label for="new-password"><span class="label__title">New password</span></label>
<label for="new-password"
><span class="label__title">{{ formatMessage(messages.newPasswordLabel) }}</span></label
>
<StyledInput
id="new-password"
v-model="newPassword"
:maxlength="2048"
type="password"
autocomplete="new-password"
placeholder="New password"
:placeholder="formatMessage(messages.newPasswordPlaceholder)"
/>
<label for="confirm-new-password"
><span class="label__title">Confirm new password</span></label
>
<label for="confirm-new-password">
<span class="label__title">{{ formatMessage(messages.confirmNewPasswordLabel) }}</span>
</label>
<StyledInput
id="confirm-new-password"
v-model="confirmNewPassword"
:maxlength="2048"
type="password"
autocomplete="new-password"
placeholder="Confirm new password"
:placeholder="formatMessage(messages.confirmNewPasswordPlaceholder)"
/>
</template>
<p></p>
<div class="input-group push-right">
<button class="iconified-button" @click="$refs.managePasswordModal.hide()">
<XIcon />
Cancel
{{ formatMessage(commonMessages.cancelButton) }}
</button>
<template v-if="removePasswordMode">
<button
@@ -103,7 +120,7 @@
@click="savePassword"
>
<TrashIcon />
Remove password
{{ formatMessage(messages.removePasswordButton) }}
</button>
</template>
<template v-else>
@@ -114,7 +131,7 @@
@click="removePasswordMode = true"
>
<TrashIcon />
Remove password
{{ formatMessage(messages.removePasswordButton) }}
</button>
<button
type="button"
@@ -127,7 +144,7 @@
@click="savePassword"
>
<SaveIcon />
Save password
{{ formatMessage(messages.savePasswordButton) }}
</button>
</template>
</div>
@@ -135,45 +152,57 @@
</Modal>
<Modal
ref="manageTwoFactorModal"
:header="`${
auth.user.has_totp && twoFactorStep === 0 ? 'Remove' : 'Setup'
} two-factor authentication`"
:header="`${auth.user.has_totp && twoFactorStep === 0 ? formatMessage(messages.twoFactorRemoveButton) : formatMessage(messages.twoFactorSetupButton)}`"
>
<div class="universal-modal">
<template v-if="auth.user.has_totp && twoFactorStep === 0">
<label for="two-factor-code">
<span class="label__title">Enter two-factor code</span>
<span class="label__description">Please enter a two-factor code to proceed.</span>
<span class="label__title">{{ formatMessage(messages.twoFactorEnterCodeLabel) }}</span>
<span class="label__description">{{
formatMessage(messages.twoFactorEnterCodeDescription)
}}</span>
</label>
<StyledInput
id="two-factor-code"
v-model="twoFactorCode"
:maxlength="11"
placeholder="Enter code..."
:placeholder="formatMessage(messages.twoFactorCodePlaceholder)"
@keyup.enter="removeTwoFactor()"
/>
<p v-if="twoFactorIncorrect" class="known-errors">The code entered is incorrect!</p>
<p v-if="twoFactorIncorrect" class="known-errors">
{{ formatMessage(messages.twoFactorIncorrectError) }}
</p>
<div class="input-group push-right">
<button class="iconified-button" @click="$refs.manageTwoFactorModal.hide()">
<XIcon />
Cancel
{{ formatMessage(commonMessages.cancelButton) }}
</button>
<button class="iconified-button danger-button" @click="removeTwoFactor">
<TrashIcon />
Remove 2FA
{{ formatMessage(messages.twoFactorRemoveButton) }}
</button>
</div>
</template>
<template v-else>
<template v-if="twoFactorStep === 0">
<p>{{ formatMessage(messages.twoFactorSetupIntro) }}</p>
<p>
Two-factor authentication keeps your account secure by requiring access to a second
device in order to sign in.
<br /><br />
Scan the QR code with <a href="https://authy.com/">Authy</a>,
<a href="https://www.microsoft.com/en-us/security/mobile-authenticator-app">
Microsoft Authenticator</a
>, or any other 2FA app to begin.
<IntlFormatted :message-id="messages.twoFactorSetupScan">
<template #authy-link="{ children }">
<a href="https://authy.com/" target="_blank" rel="noreferrer">
<component :is="() => children" />
</a>
</template>
<template #microsoft-authenticator-link="{ children }">
<a
href="https://www.microsoft.com/en-us/security/mobile-authenticator-app"
target="_blank"
rel="noreferrer"
>
<component :is="() => children" />
</a>
</template>
</IntlFormatted>
</p>
<qrcode-vue
v-if="twoFactorSecret"
@@ -185,34 +214,34 @@
level="H"
/>
<p>
If the QR code does not scan, you can manually enter the secret:
{{ formatMessage(messages.twoFactorManualSecretPrefix) }}
<strong>{{ twoFactorSecret }}</strong>
</p>
</template>
<template v-if="twoFactorStep === 1">
<label for="verify-code">
<span class="label__title">Verify code</span>
<span class="label__description"
>Enter the one-time code from authenticator to verify access.
</span>
<span class="label__title">{{
formatMessage(messages.twoFactorVerifyCodeLabel)
}}</span>
<span class="label__description">{{
formatMessage(messages.twoFactorVerifyCodeDescription)
}}</span>
</label>
<StyledInput
id="verify-code"
v-model="twoFactorCode"
:maxlength="6"
autocomplete="one-time-code"
placeholder="Enter code..."
:placeholder="formatMessage(messages.twoFactorCodePlaceholder)"
@keyup.enter="verifyTwoFactorCode()"
/>
<p v-if="twoFactorIncorrect" class="known-errors">The code entered is incorrect!</p>
<p v-if="twoFactorIncorrect" class="known-errors">
{{ formatMessage(messages.twoFactorIncorrectError) }}
</p>
</template>
<template v-if="twoFactorStep === 2">
<p>
Download and save these back-up codes in a safe place. You can use these in-place of a
2FA code if you ever lose access to your device! You should protect these codes like
your password.
</p>
<p>Backup codes can only be used once.</p>
<p>{{ formatMessage(messages.twoFactorBackupCodesIntro) }}</p>
<p>{{ formatMessage(messages.twoFactorBackupCodesSingleUse) }}</p>
<ul>
<li v-for="code in backupCodes" :key="code">{{ code }}</li>
</ul>
@@ -220,7 +249,7 @@
<div class="input-group push-right">
<button v-if="twoFactorStep === 1" class="iconified-button" @click="twoFactorStep = 0">
<LeftArrowIcon />
Back
{{ formatMessage(commonMessages.backButton) }}
</button>
<button
v-if="twoFactorStep !== 2"
@@ -228,7 +257,7 @@
@click="$refs.manageTwoFactorModal.hide()"
>
<XIcon />
Cancel
{{ formatMessage(commonMessages.cancelButton) }}
</button>
<button
v-if="twoFactorStep <= 1"
@@ -236,7 +265,7 @@
@click="twoFactorStep === 1 ? verifyTwoFactorCode() : (twoFactorStep = 1)"
>
<RightArrowIcon />
Continue
{{ formatMessage(commonMessages.continueButton) }}
</button>
<button
v-if="twoFactorStep === 2"
@@ -244,18 +273,22 @@
@click="$refs.manageTwoFactorModal.hide()"
>
<CheckIcon />
Complete setup
{{ formatMessage(messages.completeSetupButton) }}
</button>
</div>
</template>
</div>
</Modal>
<Modal ref="manageProvidersModal" header="Authentication providers">
<Modal ref="manageProvidersModal" :header="formatMessage(messages.manageProvidersModalHeader)">
<div class="universal-modal">
<div class="table">
<div class="table-head table-row">
<div class="table-text table-cell">Provider</div>
<div class="table-text table-cell">Actions</div>
<div class="table-text table-cell">
{{ formatMessage(messages.providersTableProvider) }}
</div>
<div class="table-text table-cell">
{{ formatMessage(messages.providersTableActions) }}
</div>
</div>
<div v-for="provider in authProviders" :key="provider.id" class="table-row">
<div class="table-text table-cell">
@@ -267,14 +300,14 @@
class="btn"
@click="handleRemoveAuthProvider(provider.id)"
>
<TrashIcon /> Remove
<TrashIcon /> {{ formatMessage(commonMessages.removeButton) }}
</button>
<a
v-else
class="btn"
:href="`${getAuthUrl(provider.id, '/settings/account')}&token=${auth.token}`"
>
<ExternalIcon /> Add
<ExternalIcon /> {{ formatMessage(messages.providerAddButton) }}
</a>
</div>
</div>
@@ -283,42 +316,46 @@
<div class="input-group push-right">
<button class="iconified-button" @click="$refs.manageProvidersModal.hide()">
<XIcon />
Close
{{ formatMessage(commonMessages.closeButton) }}
</button>
</div>
</div>
</Modal>
<section class="universal-card">
<h2 class="text-2xl">Account security</h2>
<h2 class="text-2xl">{{ formatMessage(messages.accountSecurityTitle) }}</h2>
<div class="adjacent-input">
<label for="theme-selector">
<span class="label__title">Email</span>
<span class="label__description">Changes the email associated with your account.</span>
<span class="label__title">{{ formatMessage(messages.emailFieldTitle) }}</span>
<span class="label__description">{{
formatMessage(messages.emailFieldDescription)
}}</span>
</label>
<div>
<button class="iconified-button" @click="$refs.changeEmailModal.show()">
<template v-if="auth.user.email">
<EditIcon />
Change email
{{ formatMessage(messages.changeEmailButton) }}
</template>
<template v-else>
<PlusIcon />
Add email
{{ formatMessage(messages.addEmailButton) }}
</template>
</button>
</div>
</div>
<div class="adjacent-input">
<label for="theme-selector">
<span class="label__title">Password</span>
<span class="label__title">{{ formatMessage(messages.passwordFieldTitle) }}</span>
<span v-if="auth.user.has_password" class="label__description">
Change
<template v-if="auth.user.auth_providers.length > 0">or remove</template>
the password used to login to your account.
{{
auth.user.auth_providers.length > 0
? formatMessage(messages.passwordDescriptionChangeOrRemove)
: formatMessage(messages.passwordDescriptionChange)
}}
</span>
<span v-else class="label__description">
Set a permanent password to login to your account.
{{ formatMessage(messages.passwordDescriptionSet) }}
</span>
</label>
<div>
@@ -335,70 +372,73 @@
"
>
<KeyIcon />
<template v-if="auth.user.has_password"> Change password </template>
<template v-else> Add password </template>
<template v-if="auth.user.has_password">{{
formatMessage(messages.changePasswordButton)
}}</template>
<template v-else> {{ formatMessage(messages.addPasswordButton) }} </template>
</button>
</div>
</div>
<div class="adjacent-input">
<label for="theme-selector">
<span class="label__title">Two-factor authentication</span>
<span class="label__description">
Add an additional layer of security to your account during login.
</span>
<span class="label__title">{{ formatMessage(messages.twoFactorFieldTitle) }}</span>
<span class="label__description">{{
formatMessage(messages.twoFactorFieldDescription)
}}</span>
</label>
<div>
<button class="iconified-button" @click="showTwoFactorModal">
<template v-if="auth.user.has_totp"> <TrashIcon /> Remove 2FA </template>
<template v-else> <PlusIcon /> Setup 2FA </template>
<template v-if="auth.user.has_totp">
<TrashIcon /> {{ formatMessage(messages.twoFactorRemoveButton) }}
</template>
<template v-else>
<PlusIcon /> {{ formatMessage(messages.twoFactorSetupButton) }}
</template>
</button>
</div>
</div>
<div class="adjacent-input">
<label for="theme-selector">
<span class="label__title">Manage authentication providers</span>
<span class="label__description">
Add or remove sign-on methods from your account, including GitHub, GitLab, Microsoft,
Discord, Steam, and Google.
</span>
<span class="label__title">{{ formatMessage(messages.manageProvidersFieldTitle) }}</span>
<span class="label__description">{{
formatMessage(messages.manageProvidersFieldDescription)
}}</span>
</label>
<div>
<button class="iconified-button" @click="$refs.manageProvidersModal.show()">
<SettingsIcon /> Manage providers
<SettingsIcon /> {{ formatMessage(messages.manageProvidersButton) }}
</button>
</div>
</div>
</section>
<section id="data-export" class="universal-card">
<h2>Data export</h2>
<p>
Request a copy of all your personal data you have uploaded to Modrinth. This may take
several minutes to complete.
</p>
<h2>{{ formatMessage(messages.dataExportTitle) }}</h2>
<p>{{ formatMessage(messages.dataExportDescription) }}</p>
<a v-if="generated" class="iconified-button" :href="generated" download="export.json">
<DownloadIcon />
Download export
{{ formatMessage(messages.downloadExportButton) }}
</a>
<button v-else class="iconified-button" :disabled="generatingExport" @click="exportData">
<template v-if="generatingExport"> <UpdatedIcon /> Generating export... </template>
<template v-else> <UpdatedIcon /> Generate export </template>
<template v-if="generatingExport">
<UpdatedIcon /> {{ formatMessage(messages.generatingExportButton) }}
</template>
<template v-else>
<UpdatedIcon /> {{ formatMessage(messages.generateExportButton) }}
</template>
</button>
</section>
<section id="delete-account" class="universal-card">
<h2>Delete account</h2>
<p>
Once you delete your account, there is no going back. Deleting your account will remove all
attached data, excluding projects, from our servers.
</p>
<h2>{{ formatMessage(messages.deleteAccountSectionTitle) }}</h2>
<p>{{ formatMessage(messages.deleteAccountSectionDescription) }}</p>
<button
type="button"
class="iconified-button danger-button"
@click="$refs.modal_confirm.show()"
>
<TrashIcon />
Delete account
{{ formatMessage(messages.deleteAccountButton) }}
</button>
</section>
</div>
@@ -419,7 +459,15 @@ import {
UpdatedIcon,
XIcon,
} from '@modrinth/assets'
import { ConfirmModal, injectNotificationManager, StyledInput } from '@modrinth/ui'
import {
commonMessages,
ConfirmModal,
defineMessages,
injectNotificationManager,
IntlFormatted,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import KeyIcon from 'assets/icons/auth/key.svg'
import DiscordIcon from 'assets/icons/auth/sso-discord.svg'
import GithubIcon from 'assets/icons/auth/sso-github.svg'
@@ -432,10 +480,6 @@ import QrcodeVue from 'qrcode.vue'
import Modal from '~/components/ui/Modal.vue'
import { getAuthUrl, removeAuthProvider } from '~/composables/auth.js'
useHead({
title: 'Account settings - Modrinth',
})
definePageMeta({
middleware: 'auth',
})
@@ -443,6 +487,282 @@ definePageMeta({
const { addNotification } = injectNotificationManager()
const auth = await useAuth()
const { formatMessage } = useVIntl()
const messages = defineMessages({
deleteAccountConfirmTitle: {
id: 'settings.account.delete.confirm.title',
defaultMessage: 'Are you sure you want to delete your account?',
},
deleteAccountConfirmDescription: {
id: 'settings.account.delete.confirm.description',
defaultMessage:
'This will **immediately delete all of your user data and follows**. This will not delete your projects. Deleting your account cannot be reversed.<br><br>If you need help with your account, get support on the [Modrinth Discord](https://discord.modrinth.com).',
},
deleteAccountConfirmProceed: {
id: 'settings.account.delete.confirm.proceed',
defaultMessage: 'Delete this account',
},
changeEmailHeaderChange: {
id: 'settings.account.email.modal.header.change',
defaultMessage: 'Change email',
},
changeEmailHeaderAdd: {
id: 'settings.account.email.modal.header.add',
defaultMessage: 'Add email',
},
emailNotPublicNotice: {
id: 'settings.account.email.modal.notice',
defaultMessage: 'Your account information is not displayed publicly.',
},
emailAddressLabel: {
id: 'settings.account.email.field.label',
defaultMessage: 'Email address',
},
emailAddressPlaceholder: {
id: 'settings.account.email.field.placeholder',
defaultMessage: 'Enter your email address...',
},
saveEmailButton: {
id: 'settings.account.email.action.save',
defaultMessage: 'Save email',
},
passwordHeaderRemove: {
id: 'settings.account.password.modal.header.remove',
defaultMessage: 'Remove password',
},
passwordHeaderChange: {
id: 'settings.account.password.modal.header.change',
defaultMessage: 'Change password',
},
passwordHeaderAdd: {
id: 'settings.account.password.modal.header.add',
defaultMessage: 'Add password',
},
passwordsDoNotMatchError: {
id: 'settings.account.password.error.mismatch',
defaultMessage: 'Input passwords do not match!',
},
confirmPasswordLabel: {
id: 'settings.account.password.field.confirm-current.label',
defaultMessage: 'Confirm password',
},
confirmPasswordDescription: {
id: 'settings.account.password.field.confirm-current.description',
defaultMessage: 'Please enter your password to proceed.',
},
oldPasswordLabel: {
id: 'settings.account.password.field.old.label',
defaultMessage: 'Old password',
},
confirmPasswordPlaceholder: {
id: 'settings.account.password.field.confirm-current.placeholder',
defaultMessage: 'Confirm password',
},
oldPasswordPlaceholder: {
id: 'settings.account.password.field.old.placeholder',
defaultMessage: 'Old password',
},
newPasswordLabel: {
id: 'settings.account.password.field.new.label',
defaultMessage: 'New password',
},
newPasswordPlaceholder: {
id: 'settings.account.password.field.new.placeholder',
defaultMessage: 'New password',
},
confirmNewPasswordLabel: {
id: 'settings.account.password.field.confirm-new.label',
defaultMessage: 'Confirm new password',
},
confirmNewPasswordPlaceholder: {
id: 'settings.account.password.field.confirm-new.placeholder',
defaultMessage: 'Confirm new password',
},
removePasswordButton: {
id: 'settings.account.password.action.remove',
defaultMessage: 'Remove password',
},
savePasswordButton: {
id: 'settings.account.password.action.save',
defaultMessage: 'Save password',
},
accountSecurityTitle: {
id: 'settings.account.security.title',
defaultMessage: 'Account security',
},
emailFieldTitle: {
id: 'settings.account.security.email.title',
defaultMessage: 'Email',
},
emailFieldDescription: {
id: 'settings.account.security.email.description',
defaultMessage: 'Changes the email associated with your account.',
},
changeEmailButton: {
id: 'settings.account.security.email.action.change',
defaultMessage: 'Change email',
},
addEmailButton: {
id: 'settings.account.security.email.action.add',
defaultMessage: 'Add email',
},
passwordFieldTitle: {
id: 'settings.account.security.password.title',
defaultMessage: 'Password',
},
passwordDescriptionChange: {
id: 'settings.account.security.password.description.change',
defaultMessage: 'Change the password used to login to your account.',
},
passwordDescriptionChangeOrRemove: {
id: 'settings.account.security.password.description.change-or-remove',
defaultMessage: 'Change or remove the password used to login to your account.',
},
passwordDescriptionSet: {
id: 'settings.account.security.password.description.set',
defaultMessage: 'Set a permanent password to login to your account.',
},
changePasswordButton: {
id: 'settings.account.security.password.action.change',
defaultMessage: 'Change password',
},
addPasswordButton: {
id: 'settings.account.security.password.action.add',
defaultMessage: 'Add password',
},
twoFactorFieldTitle: {
id: 'settings.account.security.two-factor.title',
defaultMessage: 'Two-factor authentication',
},
twoFactorFieldDescription: {
id: 'settings.account.security.two-factor.description',
defaultMessage: 'Add an additional layer of security to your account during login.',
},
twoFactorSetupButton: {
id: 'settings.account.security.two-factor.action.setup',
defaultMessage: 'Setup 2FA',
},
twoFactorEnterCodeLabel: {
id: 'settings.account.two-factor.field.code.label',
defaultMessage: 'Enter two-factor code',
},
twoFactorEnterCodeDescription: {
id: 'settings.account.two-factor.field.code.description',
defaultMessage: 'Please enter a two-factor code to proceed.',
},
twoFactorCodePlaceholder: {
id: 'settings.account.two-factor.field.code.placeholder',
defaultMessage: 'Enter code...',
},
twoFactorIncorrectError: {
id: 'settings.account.two-factor.error.incorrect-code',
defaultMessage: 'The code entered is incorrect!',
},
twoFactorRemoveButton: {
id: 'settings.account.security.two-factor.action.remove',
defaultMessage: 'Remove 2FA',
},
twoFactorSetupIntro: {
id: 'settings.account.two-factor.setup.intro',
defaultMessage:
'Two-factor authentication keeps your account secure by requiring access to a second device in order to sign in.',
},
twoFactorSetupScan: {
id: 'settings.account.two-factor.setup.scan',
defaultMessage:
'Scan the QR code with <authy-link>Authy</authy-link>, <microsoft-authenticator-link>Microsoft Authenticator</microsoft-authenticator-link>, or any other 2FA app to begin.',
},
twoFactorManualSecretPrefix: {
id: 'settings.account.two-factor.setup.manual-secret',
defaultMessage: 'If the QR code does not scan, you can manually enter the secret:',
},
twoFactorVerifyCodeLabel: {
id: 'settings.account.two-factor.verify.label',
defaultMessage: 'Verify code',
},
twoFactorVerifyCodeDescription: {
id: 'settings.account.two-factor.verify.description',
defaultMessage: 'Enter the one-time code from authenticator to verify access.',
},
twoFactorBackupCodesIntro: {
id: 'settings.account.two-factor.backup.intro',
defaultMessage:
'Download and save these back-up codes in a safe place. You can use these in-place of a 2FA code if you ever lose access to your device! You should protect these codes like your password.',
},
twoFactorBackupCodesSingleUse: {
id: 'settings.account.two-factor.backup.single-use',
defaultMessage: 'Backup codes can only be used once.',
},
completeSetupButton: {
id: 'settings.account.button.complete-setup',
defaultMessage: 'Complete setup',
},
manageProvidersModalHeader: {
id: 'settings.account.providers.modal.header',
defaultMessage: 'Authentication providers',
},
providersTableProvider: {
id: 'settings.account.providers.table.provider',
defaultMessage: 'Provider',
},
providersTableActions: {
id: 'settings.account.providers.table.actions',
defaultMessage: 'Actions',
},
providerAddButton: {
id: 'settings.account.providers.action.add',
defaultMessage: 'Add',
},
manageProvidersFieldTitle: {
id: 'settings.account.security.providers.title',
defaultMessage: 'Manage authentication providers',
},
manageProvidersFieldDescription: {
id: 'settings.account.security.providers.description',
defaultMessage:
'Add or remove sign-on methods from your account, including GitHub, GitLab, Microsoft, Discord, Steam, and Google.',
},
manageProvidersButton: {
id: 'settings.account.security.providers.action.manage',
defaultMessage: 'Manage providers',
},
dataExportTitle: {
id: 'settings.account.data-export.title',
defaultMessage: 'Data export',
},
dataExportDescription: {
id: 'settings.account.data-export.description',
defaultMessage:
'Request a copy of all your personal data you have uploaded to Modrinth. This may take several minutes to complete.',
},
downloadExportButton: {
id: 'settings.account.data-export.action.download',
defaultMessage: 'Download export',
},
generatingExportButton: {
id: 'settings.account.data-export.action.generating',
defaultMessage: 'Generating export...',
},
generateExportButton: {
id: 'settings.account.data-export.action.generate',
defaultMessage: 'Generate export',
},
deleteAccountSectionTitle: {
id: 'settings.account.delete.section.title',
defaultMessage: 'Delete account',
},
deleteAccountSectionDescription: {
id: 'settings.account.delete.section.description',
defaultMessage:
'Once you delete your account, there is no going back. Deleting your account will remove all attached data, excluding projects, from our servers.',
},
deleteAccountButton: {
id: 'settings.account.delete.section.action',
defaultMessage: 'Delete account',
},
})
const changeEmailModal = ref()
const email = ref(auth.value.user.email)
async function saveEmail() {
@@ -462,7 +782,7 @@ async function saveEmail() {
await useAuth(auth.value.token)
} catch (err) {
addNotification({
title: 'An error occurred',
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -501,7 +821,7 @@ async function savePassword() {
await useAuth(auth.value.token)
} catch (err) {
addNotification({
title: 'An error occurred',
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -537,7 +857,7 @@ async function showTwoFactorModal() {
twoFactorFlow.value = res.flow
} catch (err) {
addNotification({
title: 'An error occurred',
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -626,7 +946,7 @@ async function deleteAccount() {
})
} catch (err) {
addNotification({
title: 'An error occurred',
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -655,7 +975,7 @@ async function exportData() {
generated.value = URL.createObjectURL(blob)
} catch (err) {
addNotification({
title: 'An error occurred',
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})

View File

@@ -248,6 +248,7 @@ import {
Avatar,
Button,
Checkbox,
commonMessages,
commonSettingsMessages,
ConfirmModal,
CopyCode,
@@ -283,10 +284,14 @@ definePageMeta({
})
useHead({
title: 'Applications - Modrinth',
title: () => `${formatMessage(messages.headTitle)} - Modrinth`,
})
const messages = defineMessages({
headTitle: {
id: 'settings.applications.head-title',
defaultMessage: 'Applications',
},
modalHeader: {
id: 'settings.applications.modal.header',
defaultMessage: 'Application information',
@@ -413,10 +418,6 @@ const messages = defineMessages({
id: 'settings.applications.notification.icon-updated.description',
defaultMessage: 'Your application icon has been updated.',
},
errorTitle: {
id: 'settings.applications.notification.error.title',
defaultMessage: 'An error occurred',
},
})
const { scopesToLabels } = useScopes()
@@ -585,7 +586,7 @@ async function createApp() {
await refresh()
} catch (err) {
addNotification({
title: formatMessage(messages.errorTitle),
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -649,7 +650,7 @@ async function editApp() {
appModal.value.hide()
} catch (err) {
addNotification({
title: formatMessage(messages.errorTitle),
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -669,7 +670,7 @@ async function removeApp() {
editingId.value = null
} catch (err) {
addNotification({
title: formatMessage(messages.errorTitle),
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})

View File

@@ -2,19 +2,17 @@
<div class="universal-card">
<ConfirmModal
ref="modal_confirm"
title="Are you sure you want to revoke this application?"
description="This will revoke the application's access to your account. You can always re-authorize it later."
proceed-label="Revoke"
:title="formatMessage(messages.revokeConfirmTitle)"
:description="formatMessage(messages.revokeConfirmDescription)"
:proceed-label="formatMessage(messages.revokeAction)"
@proceed="revokeApp(revokingId)"
/>
<h2 class="text-2xl">{{ formatMessage(commonSettingsMessages.authorizedApps) }}</h2>
<p>
When you authorize an application with your Modrinth account, you grant it access to your
account. You can manage and review access to your account here at any time.
{{ formatMessage(messages.description) }}
</p>
<div v-if="appInfoLookup.length === 0" class="universal-card recessed">
We currently can't display your authorized apps, we're working to fix this. Please visit this
page at a later date!
{{ formatMessage(messages.emptyState) }}
</div>
<div
v-for="authorization in appInfoLookup"
@@ -30,7 +28,7 @@
{{ authorization.app.name }}
</h2>
<div>
by
{{ formatMessage(messages.byLabel) }}
<nuxt-link class="text-link" :to="'/user/' + authorization.owner.id">{{
authorization.owner.username
}}</nuxt-link>
@@ -47,13 +45,13 @@
<div>
<template v-if="authorization.app.description">
<label for="app-description">
<span class="label__title"> About this app </span>
<span class="label__title">{{ formatMessage(messages.aboutThisAppLabel) }}</span>
</label>
<div id="app-description">{{ authorization.app.description }}</div>
</template>
<label for="app-scope-list">
<span class="label__title">Scopes</span>
<span class="label__title">{{ formatMessage(commonMessages.scopesLabel) }}</span>
</label>
<div class="scope-list">
<div
@@ -82,7 +80,7 @@
"
>
<TrashIcon />
Revoke
{{ formatMessage(messages.revokeAction) }}
</Button>
</div>
</div>
@@ -93,8 +91,10 @@ import { CheckIcon, TrashIcon } from '@modrinth/assets'
import {
Avatar,
Button,
commonMessages,
commonSettingsMessages,
ConfirmModal,
defineMessages,
injectModrinthClient,
injectNotificationManager,
useVIntl,
@@ -107,6 +107,44 @@ const client = injectModrinthClient()
const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl()
const messages = defineMessages({
headTitle: {
id: 'settings.authorizations.head-title',
defaultMessage: 'Authorizations',
},
description: {
id: 'settings.authorizations.description',
defaultMessage:
'When you authorize an application with your Modrinth account, you grant it access to your account. You can manage and review access to your account here at any time.',
},
emptyState: {
id: 'settings.authorizations.empty-state',
defaultMessage:
"We currently can't display your authorized apps, we're working to fix this. Please visit this page at a later date!",
},
revokeConfirmTitle: {
id: 'settings.authorizations.revoke.confirm.title',
defaultMessage: 'Are you sure you want to revoke this application?',
},
revokeConfirmDescription: {
id: 'settings.authorizations.revoke.confirm.description',
defaultMessage:
"This will revoke the application's access to your account. You can always re-authorize it later.",
},
revokeAction: {
id: 'settings.authorizations.revoke.action',
defaultMessage: 'Revoke',
},
byLabel: {
id: 'settings.authorizations.by',
defaultMessage: 'by',
},
aboutThisAppLabel: {
id: 'settings.authorizations.about-this-app',
defaultMessage: 'About this app',
},
})
const { scopesToDefinitions } = useScopes()
const revokingId = ref(null)
@@ -116,7 +154,7 @@ definePageMeta({
})
useHead({
title: 'Authorizations - Modrinth',
title: () => `${formatMessage(messages.headTitle)} - Modrinth`,
})
const { data: usersApps, refetch: refresh } = useQuery({
@@ -159,7 +197,7 @@ async function revokeApp(id) {
await refresh()
} catch (err) {
addNotification({
title: 'An error occurred',
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})

View File

@@ -2,11 +2,13 @@
<div>
<section class="card">
<Breadcrumbs
current-title="Past charges"
:link-stack="[{ href: '/settings/billing', label: 'Billing and subscriptions' }]"
:current-title="formatMessage(messages.title)"
:link-stack="[
{ href: '/settings/billing', label: formatMessage(commonSettingsMessages.billing) },
]"
/>
<h2>Past charges</h2>
<p>All of your past charges to your Modrinth account will be listed here:</p>
<h2>{{ formatMessage(messages.title) }}</h2>
<p>{{ formatMessage(messages.description) }}</p>
<div
v-for="charge in charges"
:key="charge.id"
@@ -15,11 +17,13 @@
<div class="flex flex-col gap-1">
<div class="flex items-center gap-1">
<span class="font-bold text-primary">
<template v-if="charge.product?.metadata?.type === 'midas'"> Modrinth Plus </template>
<template v-else-if="charge.product?.metadata?.type === 'pyro'">
Modrinth Hosting
<template v-if="charge.product?.metadata?.type === 'midas'">
{{ formatMessage(messages.productMidas) }}
</template>
<template v-else> Medal Server Trial </template>
<template v-else-if="charge.product?.metadata?.type === 'pyro'">
{{ formatMessage(messages.productPyro) }}
</template>
<template v-else> {{ formatMessage(messages.productMedalTrial) }} </template>
<template v-if="charge.subscription_interval">
{{ charge.subscription_interval }}
</template>
@@ -41,6 +45,8 @@
import {
Badge,
Breadcrumbs,
commonSettingsMessages,
defineMessages,
injectModrinthClient,
useFormatDateTime,
useFormatPrice,
@@ -53,6 +59,7 @@ definePageMeta({
middleware: 'auth',
})
const { formatMessage } = useVIntl()
const client = injectModrinthClient()
const formatPrice = useFormatPrice()
@@ -62,6 +69,25 @@ const formatDate = useFormatDateTime({
day: '2-digit',
})
const messages = defineMessages({
description: {
id: 'settings.billing.charges.description',
defaultMessage: 'All of your past charges to your Modrinth account will be listed here:',
},
productMidas: {
id: 'settings.billing.charges.product.midas',
defaultMessage: 'Modrinth Plus',
},
productPyro: {
id: 'settings.billing.charges.product.pyro',
defaultMessage: 'Modrinth Hosting',
},
productMedalTrial: {
id: 'settings.billing.charges.product.medal-trial',
defaultMessage: 'Medal Server Trial',
},
})
const { data: charges } = useQuery({
queryKey: ['billing', 'payments'],
queryFn: async () => {

View File

@@ -14,34 +14,36 @@
<div class="flex flex-wrap justify-between gap-4">
<div class="flex flex-col gap-4">
<template v-if="midasCharge">
<span v-if="midasCharge.status === 'open'"> You're currently subscribed to: </span>
<span v-if="midasCharge.status === 'open'">
{{ formatMessage(messages.midasStatusOpen) }}
</span>
<span v-else-if="midasCharge.status === 'processing'" class="text-orange">
Your payment is being processed. Perks will activate once payment is complete.
{{ formatMessage(messages.midasStatusProcessing) }}
</span>
<span v-else-if="midasCharge.status === 'cancelled'">
You've cancelled your subscription. <br />
You will retain your perks until the end of the current billing cycle.
{{ formatMessage(messages.midasStatusCancelledLine1) }} <br />
{{ formatMessage(messages.midasStatusCancelledLine2) }}
</span>
<span v-else-if="midasCharge.status === 'failed'" class="text-red">
Your subscription payment failed. Please update your payment method.
{{ formatMessage(messages.midasStatusFailed) }}
</span>
</template>
<span v-else>Become a subscriber to Modrinth Plus!</span>
<span v-else>{{ formatMessage(messages.midasUpsell) }}</span>
<ModrinthPlusIcon class="h-8 w-min" />
<div class="flex flex-col gap-2">
<span class="font-bold">Benefits</span>
<span class="font-bold">{{ formatMessage(messages.midasBenefitsTitle) }}</span>
<div class="flex items-center gap-2">
<CheckCircleIcon class="h-5 w-5 shrink-0 text-brand" />
<span> Ad-free browsing on modrinth.com and Modrinth App </span>
<span>{{ formatMessage(messages.midasBenefitAdFree) }}</span>
</div>
<div class="flex items-center gap-2">
<CheckCircleIcon class="h-5 w-5 shrink-0 text-brand" />
<span>Modrinth+ badge on your profile</span>
<span>{{ formatMessage(messages.midasBenefitBadge) }}</span>
</div>
<div class="flex items-center gap-2">
<CheckCircleIcon class="h-5 w-5 shrink-0 text-brand" />
<span>Support Modrinth and creators directly</span>
<span>{{ formatMessage(messages.midasBenefitSupport) }}</span>
</div>
</div>
</div>
@@ -50,17 +52,22 @@
<span class="text-2xl font-bold text-dark">
<template v-if="midasCharge">
{{
formatPrice(
midasSubscriptionPrice.prices.intervals[midasSubscription.interval],
midasSubscriptionPrice.currency_code,
)
formatMessage(messages.pricePerInterval, {
price: formatPrice(
midasSubscriptionPrice.prices.intervals[midasSubscription.interval],
midasSubscriptionPrice.currency_code,
),
interval: getIntervalNounLabel(midasSubscription.interval),
})
}}
/
{{ midasSubscription.interval }}
</template>
<template v-else>
{{ formatPrice(price.prices.intervals.monthly, price.currency_code) }}
/ month
{{
formatMessage(messages.pricePerInterval, {
price: formatPrice(price.prices.intervals.monthly, price.currency_code),
interval: formatMessage(messages.intervalMonth),
})
}}
</template>
</span>
<!-- Next charge preview for Midas when interval is changing -->
@@ -74,32 +81,56 @@
"
class="-mt-1 flex items-baseline gap-2 text-sm text-secondary"
>
<span class="opacity-70">Next:</span>
<span class="opacity-70">{{ formatMessage(messages.nextLabel) }}</span>
<span class="font-semibold text-contrast">
{{ formatPrice(midasCharge.amount, midasCharge.currency_code) }}
</span>
<span>/{{ midasCharge.subscription_interval.replace('ly', '') }}</span>
<span>
{{
formatMessage(messages.slashInterval, {
interval: getIntervalNounLabel(midasCharge.subscription_interval),
})
}}
</span>
</div>
<template v-if="midasCharge">
<span
v-if="
midasCharge.status === 'open' && midasCharge.subscription_interval === 'monthly'
midasCharge.status === 'open' &&
midasCharge.subscription_interval === 'monthly' &&
oppositePrice != null
"
class="text-sm text-purple"
>
Save
{{
formatPrice(midasCharge.amount * 12 - oppositePrice, midasCharge.currency_code)
}}/year by switching to yearly billing!
formatMessage(messages.savePerYearBySwitchingToYearly, {
amount: formatPrice(
midasCharge.amount * 12 - oppositePrice,
midasCharge.currency_code,
),
})
}}
</span>
<span class="text-sm text-secondary">
Since {{ formatDate(midasSubscription.created) }}
{{
formatMessage(messages.sinceDate, {
date: formatDate(midasSubscription.created),
})
}}
</span>
<span v-if="midasCharge.status === 'open'" class="text-sm text-secondary">
Renews {{ formatDate(midasCharge.due) }}
{{
formatMessage(messages.renewsDate, {
date: formatDate(midasCharge.due),
})
}}
</span>
<span v-else-if="midasCharge.status === 'cancelled'" class="text-sm text-secondary">
Expires {{ formatDate(midasCharge.due) }}
{{
formatMessage(messages.expiresDate, {
date: formatDate(midasCharge.due),
})
}}
</span>
<span
v-if="
@@ -110,17 +141,25 @@
"
class="text-sm text-secondary"
>
Switches to {{ midasCharge.subscription_interval }} billing on
{{ formatDate(midasCharge.due) }}
{{
formatMessage(messages.switchesToBillingOn, {
interval: getIntervalAdjectiveLabel(midasCharge.subscription_interval),
date: formatDate(midasCharge.due),
})
}}
</span>
</template>
<span v-else class="text-sm text-secondary">
Or
{{ formatPrice(price.prices.intervals.yearly, price.currency_code) }} / year (save
{{
calculateSavings(price.prices.intervals.monthly, price.prices.intervals.yearly)
}}%)!
formatMessage(messages.orYearlySave, {
price: formatPrice(price.prices.intervals.yearly, price.currency_code),
percent: calculateSavings(
price.prices.intervals.monthly,
price.prices.intervals.yearly,
),
})
}}
</span>
</div>
<div
@@ -136,7 +175,7 @@
"
>
<UpdatedIcon />
Update method
{{ formatMessage(messages.updateMethod) }}
</button>
</ButtonStyled>
<ButtonStyled type="transparent" circular>
@@ -153,7 +192,9 @@
]"
>
<MoreVerticalIcon />
<template #cancel><XIcon /> Cancel</template>
<template #cancel
><XIcon /> {{ formatMessage(commonMessages.cancelButton) }}</template
>
</OverflowMenu>
</ButtonStyled>
</div>
@@ -171,7 +212,7 @@
}
"
>
<XIcon /> Cancel
<XIcon /> {{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled
@@ -181,18 +222,28 @@
<button
v-tooltip="
midasCharge.subscription_interval === 'yearly'
? `Monthly billing will cost you an additional ${formatPrice(
oppositePrice * 12 - midasCharge.amount,
midasCharge.currency_code,
)} per year`
? formatMessage(messages.monthlyBillingAdditionalPerYearTooltip, {
amount: formatPrice(
oppositePrice * 12 - midasCharge.amount,
midasCharge.currency_code,
),
})
: undefined
"
:disabled="changingInterval"
@click="switchMidasInterval(oppositeInterval)"
>
<SpinnerIcon v-if="changingInterval" class="animate-spin" />
<TransferIcon v-else /> {{ changingInterval ? 'Switching' : 'Switch' }} to
{{ oppositeInterval }}
<TransferIcon v-else />
{{
changingInterval
? formatMessage(messages.switchingToInterval, {
interval: getIntervalAdjectiveLabel(oppositeInterval),
})
: formatMessage(messages.switchToInterval, {
interval: getIntervalAdjectiveLabel(oppositeInterval),
})
}}
</button>
</ButtonStyled>
</div>
@@ -201,7 +252,7 @@
color="purple"
>
<button class="ml-auto" @click="cancelSubscription(midasSubscription.id, false)">
Resubscribe <RightArrowIcon />
{{ formatMessage(messages.resubscribe) }} <RightArrowIcon />
</button>
</ButtonStyled>
<ButtonStyled v-else color="purple" size="large">
@@ -213,7 +264,7 @@
}
"
>
Subscribe <RightArrowIcon />
{{ formatMessage(messages.subscribe) }} <RightArrowIcon />
</button>
</ButtonStyled>
</div>
@@ -236,31 +287,45 @@
/>
<div v-else class="w-fit">
<p>
A linked server couldn't be found for this subscription. There are a few possible
explanations for this. If you just purchased your server, this is normal. It could
take up to an hour for your server to be provisioned. Otherwise, if you purchased
this server a while ago, it has likely since been suspended. If this is not what
you were expecting, please contact Modrinth Support with the following
information:
{{ formatMessage(messages.pyroLinkedServerNotFound) }}
</p>
<div class="flex w-full flex-col gap-2">
<CopyCode
class="whitespace-nowrap"
:text="'Server ID: ' + subscription.metadata.id"
:text="
formatMessage(messages.pyroServerIdLabel, {
id: subscription.metadata.id,
})
"
/>
<CopyCode
class="whitespace-nowrap"
:text="
formatMessage(messages.pyroStripeIdLabel, {
id: subscription.id,
})
"
/>
<CopyCode class="whitespace-nowrap" :text="'Stripe ID: ' + subscription.id" />
</div>
</div>
<h3 class="m-0 mt-4 text-xl font-semibold leading-none text-contrast">
{{ getProductSize(getPyroProduct(subscription)) }} Plan
{{
formatMessage(messages.planTitle, {
size: getProductSize(getPyroProduct(subscription)),
})
}}
</h3>
<div class="flex flex-row justify-between">
<div class="mt-2 flex flex-col gap-2">
<div class="flex items-center gap-2">
<CheckCircleIcon class="h-5 w-5 text-brand" />
<span>
{{ getPyroProduct(subscription)?.metadata?.cpu / 2 }} Shared CPUs (Bursts up
to {{ getPyroProduct(subscription)?.metadata?.cpu }} CPUs)
{{
formatMessage(messages.pyroCpuLine, {
shared: getPyroProduct(subscription)?.metadata?.cpu / 2,
bursts: getPyroProduct(subscription)?.metadata?.cpu,
})
}}
</span>
</div>
<div class="flex items-center gap-2">
@@ -268,7 +333,9 @@
<span>
{{
getPyroProduct(subscription)?.metadata?.ram
? getPyroProduct(subscription).metadata.ram / 1024 + ' GB RAM'
? formatMessage(messages.pyroRamLine, {
gb: getPyroProduct(subscription).metadata.ram / 1024,
})
: ''
}}
</span>
@@ -278,7 +345,9 @@
<span>
{{
getPyroProduct(subscription)?.metadata?.swap
? getPyroProduct(subscription).metadata.swap / 1024 + ' GB Swap'
? formatMessage(messages.pyroSwapLine, {
gb: getPyroProduct(subscription).metadata.swap / 1024,
})
: ''
}}
</span>
@@ -288,7 +357,9 @@
<span>
{{
getPyroProduct(subscription)?.metadata?.storage
? getPyroProduct(subscription).metadata.storage / 1024 + ' GB SSD'
? formatMessage(messages.pyroStorageLine, {
gb: getPyroProduct(subscription).metadata.storage / 1024,
})
: ''
}}
</span>
@@ -309,7 +380,13 @@
: ''
}}
</span>
<span>/{{ subscription.interval.replace('ly', '') }}</span>
<span>
{{
formatMessage(messages.slashInterval, {
interval: getIntervalNounLabel(subscription.interval),
})
}}
</span>
</div>
<div
v-if="
@@ -323,7 +400,7 @@
"
class="-mt-1 flex items-baseline gap-2 text-sm text-secondary"
>
<span class="opacity-70">Next:</span>
<span class="opacity-70">{{ formatMessage(messages.nextLabel) }}</span>
<span class="font-semibold text-contrast">
{{
formatPrice(
@@ -333,24 +410,33 @@
}}
</span>
<span>
/
{{
(
getPyroCharge(subscription).subscription_interval ||
subscription.interval
).replace('ly', '')
formatMessage(messages.slashInterval, {
interval: getIntervalNounLabel(
getPyroCharge(subscription).subscription_interval ||
subscription.interval,
),
})
}}
</span>
</div>
<div v-if="getPyroCharge(subscription)" class="mb-4 flex flex-col items-end">
<span class="text-sm text-secondary">
Since {{ formatDate(subscription.created) }}
{{
formatMessage(messages.sinceDate, {
date: formatDate(subscription.created),
})
}}
</span>
<span
v-if="getPyroCharge(subscription).status === 'open'"
class="text-sm text-secondary"
>
Renews {{ formatDate(getPyroCharge(subscription).due) }}
{{
formatMessage(messages.renewsDate, {
date: formatDate(getPyroCharge(subscription).due),
})
}}
</span>
<span
v-if="
@@ -361,30 +447,36 @@
"
class="text-sm text-secondary"
>
Switches to
{{ getPyroCharge(subscription).subscription_interval }}
billing on
{{ formatDate(getPyroCharge(subscription).due) }}
{{
formatMessage(messages.switchesToBillingOn, {
interval: getIntervalAdjectiveLabel(
getPyroCharge(subscription).subscription_interval,
),
date: formatDate(getPyroCharge(subscription).due),
})
}}
</span>
<span
v-else-if="getPyroCharge(subscription).status === 'processing'"
class="text-sm text-orange"
>
Your payment is being processed. Your server will activate once payment is
complete.
{{ formatMessage(messages.pyroStatusProcessing) }}
</span>
<span
v-else-if="getPyroCharge(subscription).status === 'cancelled'"
class="text-sm text-secondary"
>
Expires {{ formatDate(getPyroCharge(subscription).due) }}
{{
formatMessage(messages.expiresDate, {
date: formatDate(getPyroCharge(subscription).due),
})
}}
</span>
<span
v-else-if="getPyroCharge(subscription).status === 'failed'"
class="text-sm text-red"
>
Your subscription payment failed. Please update your payment method, then
resubscribe.
{{ formatMessage(messages.pyroStatusFailed) }}
</span>
</div>
</div>
@@ -397,7 +489,7 @@
>
<button @click="showCancellationSurvey(subscription)">
<XIcon />
Cancel
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled
@@ -411,7 +503,7 @@
>
<button @click="showPyroUpgradeModal(subscription)">
<ArrowBigUpDashIcon />
Upgrade
{{ formatMessage(messages.upgrade) }}
</button>
</ButtonStyled>
<ButtonStyled
@@ -430,7 +522,7 @@
)
"
>
Resubscribe <RightArrowIcon />
{{ formatMessage(messages.resubscribe) }} <RightArrowIcon />
</button>
</ButtonStyled>
</div>
@@ -463,7 +555,7 @@
:on-error="
(err) =>
addNotification({
title: 'An error occurred',
title: formatMessage(commonMessages.errorNotificationTitle),
type: 'error',
text: err.message ?? (err.data ? err.data.description : err),
})
@@ -735,8 +827,205 @@ const messages = defineMessages({
id: 'settings.billing.pyro_subscription.description',
defaultMessage: 'Manage your Modrinth Server subscriptions.',
},
intervalMonth: {
id: 'settings.billing.interval.month',
defaultMessage: 'month',
},
intervalYear: {
id: 'settings.billing.interval.year',
defaultMessage: 'year',
},
intervalMonthly: {
id: 'settings.billing.interval.monthly',
defaultMessage: 'monthly',
},
intervalYearly: {
id: 'settings.billing.interval.yearly',
defaultMessage: 'yearly',
},
pricePerInterval: {
id: 'settings.billing.price.per-interval',
defaultMessage: '{price} / {interval}',
},
slashInterval: {
id: 'settings.billing.price.slash-interval',
defaultMessage: '/{interval}',
},
nextLabel: {
id: 'settings.billing.next',
defaultMessage: 'Next:',
},
midasStatusOpen: {
id: 'settings.billing.midas.status.open',
defaultMessage: "You're currently subscribed to:",
},
midasStatusProcessing: {
id: 'settings.billing.midas.status.processing',
defaultMessage:
'Your payment is being processed. Perks will activate once payment is complete.',
},
midasStatusCancelledLine1: {
id: 'settings.billing.midas.status.cancelled.line1',
defaultMessage: "You've cancelled your subscription.",
},
midasStatusCancelledLine2: {
id: 'settings.billing.midas.status.cancelled.line2',
defaultMessage: 'You will retain your perks until the end of the current billing cycle.',
},
midasStatusFailed: {
id: 'settings.billing.midas.status.failed',
defaultMessage: 'Your subscription payment failed. Please update your payment method.',
},
midasUpsell: {
id: 'settings.billing.midas.upsell',
defaultMessage: 'Become a subscriber to Modrinth Plus!',
},
midasBenefitsTitle: {
id: 'settings.billing.midas.benefits.title',
defaultMessage: 'Benefits',
},
midasBenefitAdFree: {
id: 'settings.billing.midas.benefits.ad-free',
defaultMessage: 'Ad-free browsing on modrinth.com and Modrinth App',
},
midasBenefitBadge: {
id: 'settings.billing.midas.benefits.badge',
defaultMessage: 'Modrinth+ badge on your profile',
},
midasBenefitSupport: {
id: 'settings.billing.midas.benefits.support',
defaultMessage: 'Support Modrinth and creators directly',
},
savePerYearBySwitchingToYearly: {
id: 'settings.billing.midas.save-per-year',
defaultMessage: 'Save {amount}/year by switching to yearly billing!',
},
sinceDate: {
id: 'settings.billing.since',
defaultMessage: 'Since {date}',
},
renewsDate: {
id: 'settings.billing.renews',
defaultMessage: 'Renews {date}',
},
expiresDate: {
id: 'settings.billing.expires',
defaultMessage: 'Expires {date}',
},
switchesToBillingOn: {
id: 'settings.billing.switches-to-billing-on',
defaultMessage: 'Switches to {interval} billing on {date}',
},
orYearlySave: {
id: 'settings.billing.or-yearly-save',
defaultMessage: 'Or {price} / year (save {percent}%)!',
},
updateMethod: {
id: 'settings.billing.update-method',
defaultMessage: 'Update method',
},
switchToInterval: {
id: 'settings.billing.switch.to-interval',
defaultMessage: 'Switch to {interval}',
},
switchingToInterval: {
id: 'settings.billing.switch.switching-to-interval',
defaultMessage: 'Switching to {interval}',
},
monthlyBillingAdditionalPerYearTooltip: {
id: 'settings.billing.switch.tooltip.monthly-additional-per-year',
defaultMessage: 'Monthly billing will cost you an additional {amount} per year',
},
resubscribe: {
id: 'settings.billing.resubscribe',
defaultMessage: 'Resubscribe',
},
subscribe: {
id: 'settings.billing.subscribe',
defaultMessage: 'Subscribe',
},
upgrade: {
id: 'settings.billing.upgrade',
defaultMessage: 'Upgrade',
},
pyroLinkedServerNotFound: {
id: 'settings.billing.pyro.linked-server.not-found',
defaultMessage:
"A linked server couldn't be found for this subscription. There are a few possible explanations for this. If you just purchased your server, this is normal. It could take up to an hour for your server to be provisioned. Otherwise, if you purchased this server a while ago, it has likely since been suspended. If this is not what you were expecting, please contact Modrinth Support with the following information:",
},
pyroServerIdLabel: {
id: 'settings.billing.pyro.linked-server.server-id',
defaultMessage: 'Server ID: {id}',
},
pyroStripeIdLabel: {
id: 'settings.billing.pyro.linked-server.stripe-id',
defaultMessage: 'Stripe ID: {id}',
},
planTitle: {
id: 'settings.billing.plan.title',
defaultMessage: '{size} Plan',
},
pyroCpuLine: {
id: 'settings.billing.pyro.cpu',
defaultMessage: '{shared} Shared CPUs (Bursts up to {bursts} CPUs)',
},
pyroRamLine: {
id: 'settings.billing.pyro.ram',
defaultMessage: '{gb} GB RAM',
},
pyroSwapLine: {
id: 'settings.billing.pyro.swap',
defaultMessage: '{gb} GB Swap',
},
pyroStorageLine: {
id: 'settings.billing.pyro.storage',
defaultMessage: '{gb} GB SSD',
},
pyroStatusProcessing: {
id: 'settings.billing.pyro.status.processing',
defaultMessage:
'Your payment is being processed. Your server will activate once payment is complete.',
},
pyroStatusFailed: {
id: 'settings.billing.pyro.status.failed',
defaultMessage:
'Your subscription payment failed. Please update your payment method, then resubscribe.',
},
pyroResubscribeRequestSubmittedTitle: {
id: 'settings.billing.pyro.resubscribe.request-submitted.title',
defaultMessage: 'Resubscription request submitted',
},
pyroResubscribeRequestSubmittedText: {
id: 'settings.billing.pyro.resubscribe.request-submitted.text',
defaultMessage:
'If the server is currently suspended, it may take up to 10 minutes for another charge attempt to be made.',
},
pyroResubscribeSuccessText: {
id: 'settings.billing.pyro.resubscribe.success.text',
defaultMessage: 'Server subscription resubscribed successfully',
},
pyroResubscribeErrorTitle: {
id: 'settings.billing.pyro.resubscribe.error.title',
defaultMessage: 'Error resubscribing',
},
pyroResubscribeErrorText: {
id: 'settings.billing.pyro.resubscribe.error.text',
defaultMessage: 'An error occurred while resubscribing to your Modrinth server.',
},
})
function getIntervalNounLabel(interval) {
return interval === 'yearly'
? formatMessage(messages.intervalYear)
: formatMessage(messages.intervalMonth)
}
function getIntervalAdjectiveLabel(interval) {
return interval === 'yearly'
? formatMessage(messages.intervalYearly)
: formatMessage(messages.intervalMonthly)
}
const queryClient = useQueryClient()
const { data: paymentMethods } = useQuery({
@@ -860,7 +1149,7 @@ async function editPaymentMethod(index, primary) {
await refresh()
} catch (err) {
addNotification({
title: 'An error occurred',
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -875,7 +1164,7 @@ async function removePaymentMethod(index) {
await refresh()
} catch (err) {
addNotification({
title: 'An error occurred',
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -893,7 +1182,7 @@ async function cancelSubscription(id, cancelled) {
await refresh()
} catch (err) {
addNotification({
title: 'An error occurred',
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -920,12 +1209,12 @@ const getPyroCharge = (subscription) => {
}
const getProductSize = (product) => {
if (!product || !product.metadata) return 'Unknown'
if (!product || !product.metadata) return formatMessage(commonMessages.planUnknownLabel)
const ramSize = product.metadata.ram
if (ramSize === 4096) return 'Small'
if (ramSize === 6144) return 'Medium'
if (ramSize === 8192) return 'Large'
return 'Custom'
if (ramSize === 4096) return formatMessage(commonMessages.planSmallLabel)
if (ramSize === 6144) return formatMessage(commonMessages.planMediumLabel)
if (ramSize === 8192) return formatMessage(commonMessages.planLargeLabel)
return formatMessage(commonMessages.planCustomLabel)
}
const getProductPrice = (product, interval) => {
@@ -962,21 +1251,21 @@ const resubscribePyro = async (subscriptionId, wasSuspended) => {
await refresh()
if (wasSuspended) {
addNotification({
title: 'Resubscription request submitted',
text: 'If the server is currently suspended, it may take up to 10 minutes for another charge attempt to be made.',
title: formatMessage(messages.pyroResubscribeRequestSubmittedTitle),
text: formatMessage(messages.pyroResubscribeRequestSubmittedText),
type: 'success',
})
} else {
addNotification({
title: 'Success',
text: 'Server subscription resubscribed successfully',
title: formatMessage(commonMessages.successLabel),
text: formatMessage(messages.pyroResubscribeSuccessText),
type: 'success',
})
}
} catch {
addNotification({
title: 'Error resubscribing',
text: 'An error occurred while resubscribing to your Modrinth server.',
title: formatMessage(messages.pyroResubscribeErrorTitle),
text: formatMessage(messages.pyroResubscribeErrorText),
type: 'error',
})
}

View File

@@ -59,7 +59,7 @@
class="radio shrink-0"
/>
<RadioButtonIcon v-else class="radio shrink-0" />
List
{{ formatMessage(layoutMode.rows) }}
</div>
</button>
<button
@@ -88,7 +88,7 @@
class="radio shrink-0"
/>
<RadioButtonIcon v-else class="radio shrink-0" />
Grid
{{ formatMessage(layoutMode.grid) }}
</div>
</button>
</div>
@@ -194,12 +194,19 @@ import type { DisplayLocation } from '~/plugins/cosmetics'
import { isDarkTheme, type Theme } from '~/plugins/theme/index.ts'
useHead({
title: 'Display settings - Modrinth',
title: () => `${formatMessage(messages.headTitle)} - Modrinth`,
})
const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl()
const messages = defineMessages({
headTitle: {
id: 'settings.head-title',
defaultMessage: 'Display settings',
},
})
const developerModeBanner = defineMessages({
description: {
id: 'settings.display.banner.developer-mode.description',
@@ -212,6 +219,32 @@ const developerModeBanner = defineMessages({
},
})
const layoutMode = defineMessages({
rows: {
id: 'settings.display.project-list-layouts.mode.rows',
defaultMessage: 'Rows',
},
grid: {
id: 'settings.display.project-list-layouts.mode.grid',
defaultMessage: 'Grid',
},
gallery: {
id: 'settings.display.project-list-layouts.mode.gallery',
defaultMessage: 'Gallery',
},
})
const notifications = defineMessages({
developerModeDeactivatedTitle: {
id: 'settings.display.notification.developer-mode-deactivated.title',
defaultMessage: 'Developer mode deactivated',
},
developerModeDeactivatedText: {
id: 'settings.display.notification.developer-mode-deactivated.text',
defaultMessage: 'Developer mode has been disabled',
},
})
const colorTheme = defineMessages({
title: {
id: 'settings.display.theme.title',
@@ -370,8 +403,8 @@ function disableDeveloperMode() {
flags.value.developerMode = !flags.value.developerMode
saveFeatureFlags()
addNotification({
title: 'Developer mode deactivated',
text: 'Developer mode has been disabled',
title: formatMessage(notifications.developerModeDeactivatedTitle),
text: formatMessage(notifications.developerModeDeactivatedText),
type: 'success',
})
}

View File

@@ -100,7 +100,7 @@ const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl()
useHead({
title: 'Profile settings - Modrinth',
title: () => `${formatMessage(messages.headTitle)} - Modrinth`,
})
definePageMeta({
@@ -108,6 +108,10 @@ definePageMeta({
})
const messages = defineMessages({
headTitle: {
id: 'settings.profile.head-title',
defaultMessage: 'Profile settings',
},
title: {
id: 'settings.profile.profile-info',
defaultMessage: 'Profile information',
@@ -231,7 +235,7 @@ async function save() {
avatarUrl.value = auth.value.user.avatar_url
} catch (err) {
addNotification({
title: 'An error occurred',
title: formatMessage(commonMessages.errorNotificationTitle),
text: err
? err.data
? err.data.description

View File

@@ -950,6 +950,21 @@
"label.password": {
"defaultMessage": "Password"
},
"label.plan-custom": {
"defaultMessage": "Custom"
},
"label.plan-large": {
"defaultMessage": "Large"
},
"label.plan-medium": {
"defaultMessage": "Medium"
},
"label.plan-small": {
"defaultMessage": "Small"
},
"label.plan-unknown": {
"defaultMessage": "Unknown"
},
"label.platform": {
"defaultMessage": "Platform"
},

View File

@@ -450,6 +450,26 @@ export const commonMessages = defineMessages({
id: 'label.no-items',
defaultMessage: 'No items',
},
planUnknownLabel: {
id: 'label.plan-unknown',
defaultMessage: 'Unknown',
},
planSmallLabel: {
id: 'label.plan-small',
defaultMessage: 'Small',
},
planMediumLabel: {
id: 'label.plan-medium',
defaultMessage: 'Medium',
},
planLargeLabel: {
id: 'label.plan-large',
defaultMessage: 'Large',
},
planCustomLabel: {
id: 'label.plan-custom',
defaultMessage: 'Custom',
},
switchVersionButton: {
id: 'button.switch-version',
defaultMessage: 'Switch version',