feat(frontend): Make dashboard page localizable (#5727)

* Make dashboard page localizable

* dashboard sidebar

* prepr:frontend

* don't change the keys

* undo fix

* fix any err

* don't i18n csv

* prepr:frontend

* fix: do not use button key

* prepr:frontend

* capitalize string date

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
xinyihl
2026-04-26 21:09:08 +08:00
committed by GitHub
parent faf593b2af
commit 453369ca07
13 changed files with 811 additions and 160 deletions

View File

@@ -554,6 +554,9 @@
"dashboard.affiliate-links.create.button": { "dashboard.affiliate-links.create.button": {
"message": "Create affiliate link" "message": "Create affiliate link"
}, },
"dashboard.affiliate-links.empty.no-codes": {
"message": "No affiliate codes found."
},
"dashboard.affiliate-links.error.title": { "dashboard.affiliate-links.error.title": {
"message": "Error loading affiliate links" "message": "Error loading affiliate links"
}, },
@@ -572,6 +575,15 @@
"dashboard.affiliate-links.search": { "dashboard.affiliate-links.search": {
"message": "Search affiliate links..." "message": "Search affiliate links..."
}, },
"dashboard.analytics.from-projects": {
"message": "from {count} {count, plural, one {project} other {projects}}"
},
"dashboard.analytics.total-downloads": {
"message": "Total downloads"
},
"dashboard.analytics.total-followers": {
"message": "Total followers"
},
"dashboard.collections.button.create-new": { "dashboard.collections.button.create-new": {
"message": "Create new" "message": "Create new"
}, },
@@ -596,6 +608,18 @@
"dashboard.collections.long-title": { "dashboard.collections.long-title": {
"message": "Your collections" "message": "Your collections"
}, },
"dashboard.collections.placeholder.search": {
"message": "Search collections..."
},
"dashboard.collections.sort.name-ascending": {
"message": "Name (A-Z)"
},
"dashboard.collections.sort.recently-created": {
"message": "Recently Created"
},
"dashboard.collections.sort.recently-updated": {
"message": "Recently Updated"
},
"dashboard.creator-tax-form-modal.confirmation.download-button": { "dashboard.creator-tax-form-modal.confirmation.download-button": {
"message": "Download {formType}" "message": "Download {formType}"
}, },
@@ -848,6 +872,168 @@
"dashboard.creator-withdraw-modal.withdraw-limit-used": { "dashboard.creator-withdraw-modal.withdraw-limit-used": {
"message": "You've used up your <b>{withdrawLimit}</b> withdrawal limit. You must complete a tax form to withdraw more." "message": "You've used up your <b>{withdrawLimit}</b> withdrawal limit. You must complete a tax form to withdraw more."
}, },
"dashboard.head-title": {
"message": "Dashboard"
},
"dashboard.notifications.button.mark-all-as-read": {
"message": "Mark all as read"
},
"dashboard.notifications.button.view-history": {
"message": "View history"
},
"dashboard.notifications.empty.no-unread": {
"message": "You don't have any unread notifications."
},
"dashboard.notifications.error.loading": {
"message": "Error loading notifications:"
},
"dashboard.notifications.history.label": {
"message": "History"
},
"dashboard.notifications.history.title": {
"message": "Notification history"
},
"dashboard.notifications.link.see-all": {
"message": "See all"
},
"dashboard.notifications.link.view-history": {
"message": "View notification history"
},
"dashboard.notifications.link.view-more": {
"message": "View {extraNotifs} more {extraNotifs, plural, one {notification} other {notifications}}"
},
"dashboard.notifications.loading": {
"message": "Loading notifications..."
},
"dashboard.organizations.button.create": {
"message": "Create organization"
},
"dashboard.organizations.empty.cta": {
"message": "Make an organization!"
},
"dashboard.organizations.error.fetch": {
"message": "Failed to fetch organizations"
},
"dashboard.organizations.member-count": {
"message": "{count} {count, plural, one {member} other {members}}"
},
"dashboard.organizations.title": {
"message": "Organizations"
},
"dashboard.projects.bulk-edit-hint": {
"message": "You can edit multiple projects at once by selecting them below."
},
"dashboard.projects.bulk-edit.server-disabled": {
"message": "Server projects do not support bulk editing"
},
"dashboard.projects.empty": {
"message": "You don't have any projects yet. Click the green button above to begin."
},
"dashboard.projects.head-title": {
"message": "Projects"
},
"dashboard.projects.links.and-more": {
"message": "and {count} more..."
},
"dashboard.projects.links.button.clear-link": {
"message": "Clear link"
},
"dashboard.projects.links.button.edit": {
"message": "Edit links"
},
"dashboard.projects.links.changes-applied": {
"message": "Changes will be applied to <strong>{count}</strong> {count, plural, one {project} other {projects}}."
},
"dashboard.projects.links.description": {
"message": "Any links you specify below will be overwritten on each of the selected projects. Any you leave blank will be ignored. You can clear a link from all selected projects using the trash can button."
},
"dashboard.projects.links.discord-invite.description": {
"message": "An invitation link to your Discord server."
},
"dashboard.projects.links.discord-invite.label": {
"message": "Discord invite"
},
"dashboard.projects.links.issue-tracker.description": {
"message": "A place for users to report bugs, issues, and concerns about your project."
},
"dashboard.projects.links.issue-tracker.label": {
"message": "Issue tracker"
},
"dashboard.projects.links.placeholder.cleared": {
"message": "Existing link will be cleared"
},
"dashboard.projects.links.placeholder.valid-discord-url": {
"message": "Enter a valid Discord invite URL"
},
"dashboard.projects.links.placeholder.valid-url": {
"message": "Enter a valid URL"
},
"dashboard.projects.links.show-all-projects": {
"message": "Show all projects"
},
"dashboard.projects.links.source-code.description": {
"message": "A page/repository containing the source code for your project"
},
"dashboard.projects.links.source-code.label": {
"message": "Source code"
},
"dashboard.projects.links.wiki-page.description": {
"message": "A page containing information, documentation, and help for the project."
},
"dashboard.projects.links.wiki-page.label": {
"message": "Wiki page"
},
"dashboard.projects.notification.bulk-edit-success": {
"message": "Bulk edited selected project's links."
},
"dashboard.projects.project.icon-alt": {
"message": "Icon for {title}"
},
"dashboard.projects.project.moderator-message-aria": {
"message": "Project has a message from the moderators. View the project to see more."
},
"dashboard.projects.project.review-environment-metadata": {
"message": "Please review environment metadata"
},
"dashboard.projects.sort.ascending": {
"message": "Ascending"
},
"dashboard.projects.sort.descending": {
"message": "Descending"
},
"dashboard.projects.sort.option.name": {
"message": "Name"
},
"dashboard.projects.sort.option.status": {
"message": "Status"
},
"dashboard.projects.sort.option.type": {
"message": "Type"
},
"dashboard.projects.table.icon": {
"message": "Icon"
},
"dashboard.projects.table.id": {
"message": "ID"
},
"dashboard.projects.table.name": {
"message": "Name"
},
"dashboard.projects.table.status": {
"message": "Status"
},
"dashboard.projects.table.type": {
"message": "Type"
},
"dashboard.report.title": {
"message": "Report {id}"
},
"dashboard.reports.active-title": {
"message": "Active reports"
},
"dashboard.reports.title": {
"message": "Reports"
},
"dashboard.revenue.available-now": { "dashboard.revenue.available-now": {
"message": "Available now" "message": "Available now"
}, },
@@ -884,6 +1070,9 @@
"dashboard.revenue.transactions.btn.download-csv": { "dashboard.revenue.transactions.btn.download-csv": {
"message": "Download as CSV" "message": "Download as CSV"
}, },
"dashboard.revenue.transactions.head-title": {
"message": "Transaction history"
},
"dashboard.revenue.transactions.header": { "dashboard.revenue.transactions.header": {
"message": "Transactions" "message": "Transactions"
}, },
@@ -893,9 +1082,18 @@
"dashboard.revenue.transactions.none.desc": { "dashboard.revenue.transactions.none.desc": {
"message": "Your payouts and withdrawals will appear here." "message": "Your payouts and withdrawals will appear here."
}, },
"dashboard.revenue.transactions.period.last-month": {
"message": "Last month"
},
"dashboard.revenue.transactions.period.this-month": {
"message": "This month"
},
"dashboard.revenue.transactions.see-all": { "dashboard.revenue.transactions.see-all": {
"message": "See all" "message": "See all"
}, },
"dashboard.revenue.transactions.year.all": {
"message": "All years"
},
"dashboard.revenue.withdraw.blocked-tin-mismatch": { "dashboard.revenue.withdraw.blocked-tin-mismatch": {
"message": "Your withdrawals are temporarily locked because your TIN or SSN didn't match IRS records. Please contact support to reset and resubmit your tax form." "message": "Your withdrawals are temporarily locked because your TIN or SSN didn't match IRS records. Please contact support to reset and resubmit your tax form."
}, },
@@ -908,6 +1106,33 @@
"dashboard.revenue.withdraw.header": { "dashboard.revenue.withdraw.header": {
"message": "Withdraw" "message": "Withdraw"
}, },
"dashboard.sidebar.label.activeReports": {
"message": "Active reports"
},
"dashboard.sidebar.label.analytics": {
"message": "Analytics"
},
"dashboard.sidebar.label.creators": {
"message": "Creators"
},
"dashboard.sidebar.label.dashboard": {
"message": "Dashboard"
},
"dashboard.sidebar.label.notifications": {
"message": "Notifications"
},
"dashboard.sidebar.label.organizations": {
"message": "Organizations"
},
"dashboard.sidebar.label.overview": {
"message": "Overview"
},
"dashboard.sidebar.label.projects": {
"message": "Projects"
},
"dashboard.sidebar.label.revenue": {
"message": "Revenue"
},
"dashboard.withdraw.completion.account": { "dashboard.withdraw.completion.account": {
"message": "Account" "message": "Account"
}, },

View File

@@ -3,26 +3,47 @@
<div class="normal-page__sidebar"> <div class="normal-page__sidebar">
<NavStack <NavStack
:items="[ :items="[
{ type: 'heading', label: 'Dashboard' }, { type: 'heading', label: formatMessage(messages.dashboard) },
{ link: '/dashboard', label: 'Overview', icon: DashboardIcon }, { link: '/dashboard', label: formatMessage(messages.overview), icon: DashboardIcon },
{ link: '/dashboard/notifications', label: 'Notifications', icon: NotificationsIcon }, {
{ link: '/dashboard/reports', label: 'Active reports', icon: ReportIcon }, link: '/dashboard/notifications',
label: formatMessage(messages.notifications),
icon: NotificationsIcon,
},
{
link: '/dashboard/reports',
label: formatMessage(messages.activeReports),
icon: ReportIcon,
},
{ {
link: '/dashboard/collections', link: '/dashboard/collections',
label: formatMessage(commonMessages.collectionsLabel), label: formatMessage(commonMessages.collectionsLabel),
icon: LibraryIcon, icon: LibraryIcon,
}, },
{ type: 'heading', label: 'Creators' }, { type: 'heading', label: formatMessage(messages.creators) },
{ link: '/dashboard/projects', label: 'Projects', icon: ListIcon }, { link: '/dashboard/projects', label: formatMessage(messages.projects), icon: ListIcon },
{ link: '/dashboard/organizations', label: 'Organizations', icon: OrganizationIcon }, {
{ link: '/dashboard/analytics', label: 'Analytics', icon: ChartIcon }, link: '/dashboard/organizations',
label: formatMessage(messages.organizations),
icon: OrganizationIcon,
},
{
link: '/dashboard/analytics',
label: formatMessage(messages.analytics),
icon: ChartIcon,
},
{ {
link: '/dashboard/affiliate-links', link: '/dashboard/affiliate-links',
label: formatMessage(commonMessages.affiliateLinksButton), label: formatMessage(commonMessages.affiliateLinksButton),
icon: AffiliateIcon, icon: AffiliateIcon,
shown: !!isAffiliate, shown: !!isAffiliate,
}, },
{ link: '/dashboard/revenue', label: 'Revenue', icon: CurrencyIcon, matchNested: true }, {
link: '/dashboard/revenue',
label: formatMessage(messages.revenue),
icon: CurrencyIcon,
matchNested: true,
},
]" ]"
/> />
</div> </div>
@@ -43,7 +64,7 @@ import {
OrganizationIcon, OrganizationIcon,
ReportIcon, ReportIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { commonMessages, useVIntl } from '@modrinth/ui' import { commonMessages, defineMessages, useVIntl } from '@modrinth/ui'
import { type User, UserBadge } from '@modrinth/utils' import { type User, UserBadge } from '@modrinth/utils'
import NavStack from '~/components/ui/NavStack.vue' import NavStack from '~/components/ui/NavStack.vue'
@@ -56,6 +77,45 @@ const isAffiliate = computed(() => {
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const messages = defineMessages({
dashboard: {
id: 'dashboard.sidebar.label.dashboard',
defaultMessage: 'Dashboard',
},
overview: {
id: 'dashboard.sidebar.label.overview',
defaultMessage: 'Overview',
},
notifications: {
id: 'dashboard.sidebar.label.notifications',
defaultMessage: 'Notifications',
},
activeReports: {
id: 'dashboard.sidebar.label.activeReports',
defaultMessage: 'Active reports',
},
creators: {
id: 'dashboard.sidebar.label.creators',
defaultMessage: 'Creators',
},
projects: {
id: 'dashboard.sidebar.label.projects',
defaultMessage: 'Projects',
},
organizations: {
id: 'dashboard.sidebar.label.organizations',
defaultMessage: 'Organizations',
},
analytics: {
id: 'dashboard.sidebar.label.analytics',
defaultMessage: 'Analytics',
},
revenue: {
id: 'dashboard.sidebar.label.revenue',
defaultMessage: 'Revenue',
},
})
definePageMeta({ definePageMeta({
middleware: 'auth', middleware: 'auth',
}) })

View File

@@ -44,7 +44,7 @@
v-else-if="!filteredAffiliates || filteredAffiliates.length === 0" v-else-if="!filteredAffiliates || filteredAffiliates.length === 0"
class="py-8 text-center" class="py-8 text-center"
> >
<p class="text-secondary">No affiliate codes found.</p> <p class="text-secondary">{{ formatMessage(messages.noAffiliateCodesFound) }}</p>
</div> </div>
<div v-else class="space-y-3"> <div v-else class="space-y-3">
<AffiliateLinkCard <AffiliateLinkCard
@@ -166,6 +166,10 @@ const messages = defineMessages({
id: 'dashboard.affiliate-links.error.title', id: 'dashboard.affiliate-links.error.title',
defaultMessage: 'Error loading affiliate links', defaultMessage: 'Error loading affiliate links',
}, },
noAffiliateCodesFound: {
id: 'dashboard.affiliate-links.empty.no-codes',
defaultMessage: 'No affiliate codes found.',
},
revokeConfirmButton: { revokeConfirmButton: {
id: 'dashboard.affiliate-links.revoke-confirm.button', id: 'dashboard.affiliate-links.revoke-confirm.button',
defaultMessage: 'Revoke', defaultMessage: 'Revoke',

View File

@@ -12,10 +12,17 @@
</template> </template>
<script setup> <script setup>
import { injectModrinthClient, useDebugLogger } from '@modrinth/ui' import {
commonProjectSettingsMessages,
injectModrinthClient,
useDebugLogger,
useVIntl,
} from '@modrinth/ui'
import ChartDisplay from '~/components/ui/charts/ChartDisplay.vue' import ChartDisplay from '~/components/ui/charts/ChartDisplay.vue'
const { formatMessage } = useVIntl()
const debug = useDebugLogger('analytics.vue') const debug = useDebugLogger('analytics.vue')
definePageMeta({ definePageMeta({
@@ -23,7 +30,7 @@ definePageMeta({
}) })
useHead({ useHead({
title: 'Analytics - Modrinth', title: () => `${formatMessage(commonProjectSettingsMessages.analytics)} - Modrinth`,
}) })
const auth = await useAuth() const auth = await useAuth()

View File

@@ -10,7 +10,7 @@
:icon="SearchIcon" :icon="SearchIcon"
type="text" type="text"
clearable clearable
placeholder="Search collections..." :placeholder="formatMessage(messages.searchCollectionsPlaceholder)"
wrapper-class="w-full" wrapper-class="w-full"
input-class="!h-12" input-class="!h-12"
/> />
@@ -20,18 +20,13 @@
v-slot="{ selected }" v-slot="{ selected }"
v-model="sortBy" v-model="sortBy"
class="!w-auto flex-grow md:flex-grow-0" class="!w-auto flex-grow md:flex-grow-0"
name="Sort by" :name="formatMessage(commonMessages.sortByLabel)"
:options="['updated', 'created', 'name']" :options="['updated', 'created', 'name']"
:display-name=" :display-name="formatCollectionSortOption"
(option) =>
option === 'updated'
? 'Recently Updated'
: option === 'created'
? 'Recently Created'
: 'Name (A-Z)'
"
> >
<span class="font-semibold text-primary">Sort by: </span> <span class="font-semibold text-primary">{{
formatMessage(commonMessages.sortByLabel)
}}</span>
<span class="font-semibold text-secondary">{{ selected }}</span> <span class="font-semibold text-secondary">{{ selected }}</span>
</DropdownSelect> </DropdownSelect>
@@ -188,6 +183,22 @@ const messages = defineMessages({
id: 'dashboard.collections.label.search-input', id: 'dashboard.collections.label.search-input',
defaultMessage: 'Search your collections', defaultMessage: 'Search your collections',
}, },
searchCollectionsPlaceholder: {
id: 'dashboard.collections.placeholder.search',
defaultMessage: 'Search collections...',
},
sortRecentlyUpdated: {
id: 'dashboard.collections.sort.recently-updated',
defaultMessage: 'Recently Updated',
},
sortRecentlyCreated: {
id: 'dashboard.collections.sort.recently-created',
defaultMessage: 'Recently Created',
},
sortNameAscending: {
id: 'dashboard.collections.sort.name-ascending',
defaultMessage: 'Name (A-Z)',
},
emptyNoMatch: { emptyNoMatch: {
id: 'dashboard.collections.empty.no-match', id: 'dashboard.collections.empty.no-match',
defaultMessage: 'No collections match your search', defaultMessage: 'No collections match your search',
@@ -234,6 +245,18 @@ const router = useNativeRouter()
const validSortOptions = ['updated', 'created', 'name'] const validSortOptions = ['updated', 'created', 'name']
const sortBy = ref(validSortOptions.includes(route.query.s) ? route.query.s : 'updated') const sortBy = ref(validSortOptions.includes(route.query.s) ? route.query.s : 'updated')
function formatCollectionSortOption(option) {
if (option === 'updated') {
return formatMessage(messages.sortRecentlyUpdated)
}
if (option === 'created') {
return formatMessage(messages.sortRecentlyCreated)
}
return formatMessage(messages.sortNameAscending)
}
const orderedCollections = computed(() => { const orderedCollections = computed(() => {
if (!collections.value) return [] if (!collections.value) return []
return [...collections.value] return [...collections.value]

View File

@@ -7,7 +7,7 @@
{{ auth.user.username }} {{ auth.user.username }}
</h1> </h1>
<NuxtLink class="goto-link" :to="`/user/${auth.user.username}`"> <NuxtLink class="goto-link" :to="`/user/${auth.user.username}`">
Visit your profile {{ formatMessage(commonMessages.visitYourProfile) }}
<ChevronRightIcon class="featured-header-chevron" aria-hidden="true" /> <ChevronRightIcon class="featured-header-chevron" aria-hidden="true" />
</NuxtLink> </NuxtLink>
</div> </div>
@@ -15,13 +15,15 @@
<div class="dashboard-notifications"> <div class="dashboard-notifications">
<section class="universal-card"> <section class="universal-card">
<div class="header__row"> <div class="header__row">
<h2 class="header__title text-2xl">Notifications</h2> <h2 class="header__title text-2xl">
{{ formatMessage(commonMessages.notificationsLabel) }}
</h2>
<nuxt-link <nuxt-link
v-if="notifications.length > 0" v-if="notifications.length > 0"
class="goto-link" class="goto-link"
to="/dashboard/notifications" to="/dashboard/notifications"
> >
See all {{ formatMessage(messages.seeAll) }}
<ChevronRightIcon /> <ChevronRightIcon />
</nuxt-link> </nuxt-link>
</div> </div>
@@ -42,15 +44,15 @@
class="goto-link view-more-notifs mt-4" class="goto-link view-more-notifs mt-4"
to="/dashboard/notifications" to="/dashboard/notifications"
> >
View {{ extraNotifs }} more notification{{ extraNotifs === 1 ? '' : 's' }} {{ formatMessage(messages.viewMore, { extraNotifs: extraNotifs }) }}
<ChevronRightIcon /> <ChevronRightIcon />
</nuxt-link> </nuxt-link>
</template> </template>
<div v-else class="universal-body"> <div v-else class="universal-body">
<p>You have no unread notifications.</p> <p>{{ formatMessage(messages.noUnreadNotifications) }}</p>
<nuxt-link class="iconified-button !mt-4" to="/dashboard/notifications/history"> <nuxt-link class="iconified-button !mt-4" to="/dashboard/notifications/history">
<HistoryIcon /> <HistoryIcon />
View notification history {{ formatMessage(messages.viewNotificationHistory) }}
</nuxt-link> </nuxt-link>
</div> </div>
</section> </section>
@@ -58,18 +60,16 @@
<div class="dashboard-analytics"> <div class="dashboard-analytics">
<section class="universal-card"> <section class="universal-card">
<h2>Analytics</h2> <h2>{{ formatMessage(commonMessages.analyticsButton) }}</h2>
<div class="grid-display"> <div class="grid-display">
<div class="grid-display__item"> <div class="grid-display__item">
<div class="label">Total downloads</div> <div class="label">{{ formatMessage(messages.totalDownloads) }}</div>
<div class="value"> <div class="value">
{{ $formatNumber(projects.reduce((agg, x) => agg + x.downloads, 0)) }} {{ $formatNumber(projects.reduce((agg, x) => agg + x.downloads, 0)) }}
</div> </div>
<span <span>{{
>from formatMessage(messages.fromProjects, { count: downloadsProjectCount })
{{ downloadsProjectCount }} }}</span>
project{{ downloadsProjectCount === 1 ? '' : 's' }}</span
>
<!-- <NuxtLink class="goto-link" to="/dashboard/analytics"--> <!-- <NuxtLink class="goto-link" to="/dashboard/analytics"-->
<!-- >View breakdown--> <!-- >View breakdown-->
<!-- <ChevronRightIcon--> <!-- <ChevronRightIcon-->
@@ -78,16 +78,15 @@
<!-- /></NuxtLink>--> <!-- /></NuxtLink>-->
</div> </div>
<div class="grid-display__item"> <div class="grid-display__item">
<div class="label">Total followers</div> <div class="label">{{ formatMessage(messages.totalFollowers) }}</div>
<div class="value"> <div class="value">
{{ $formatNumber(projects.reduce((agg, x) => agg + x.followers, 0)) }} {{ $formatNumber(projects.reduce((agg, x) => agg + x.followers, 0)) }}
</div> </div>
<span> <span>
<span <span>{{
>from {{ followersProjectCount }} project{{ formatMessage(messages.fromProjects, { count: followersProjectCount })
followersProjectCount === 1 ? '' : 's' }}</span></span
}}</span >
></span
> >
</div> </div>
</div> </div>
@@ -97,14 +96,58 @@
</template> </template>
<script setup> <script setup>
import { ChevronRightIcon, HistoryIcon } from '@modrinth/assets' import { ChevronRightIcon, HistoryIcon } from '@modrinth/assets'
import { Avatar, injectModrinthClient } from '@modrinth/ui' import {
Avatar,
commonMessages,
defineMessages,
injectModrinthClient,
useVIntl,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query' import { useQuery } from '@tanstack/vue-query'
import NotificationItem from '~/components/ui/NotificationItem.vue' import NotificationItem from '~/components/ui/NotificationItem.vue'
import { fetchExtraNotificationData, groupNotifications } from '~/helpers/platform-notifications.ts' import { fetchExtraNotificationData, groupNotifications } from '~/helpers/platform-notifications.ts'
useHead({ useHead({
title: 'Dashboard - Modrinth', title: () => `${formatMessage(messages.headTitle)} - Modrinth`,
})
const { formatMessage } = useVIntl()
const messages = defineMessages({
headTitle: {
id: 'dashboard.head-title',
defaultMessage: 'Dashboard',
},
seeAll: {
id: 'dashboard.notifications.link.see-all',
defaultMessage: 'See all',
},
viewMore: {
id: 'dashboard.notifications.link.view-more',
defaultMessage:
'View {extraNotifs} more {extraNotifs, plural, one {notification} other {notifications}}',
},
noUnreadNotifications: {
id: 'dashboard.notifications.empty.no-unread',
defaultMessage: 'You have no unread notifications.',
},
viewNotificationHistory: {
id: 'dashboard.notifications.link.view-history',
defaultMessage: 'View notification history',
},
totalDownloads: {
id: 'dashboard.analytics.total-downloads',
defaultMessage: 'Total downloads',
},
totalFollowers: {
id: 'dashboard.analytics.total-followers',
defaultMessage: 'Total followers',
},
fromProjects: {
id: 'dashboard.analytics.from-projects',
defaultMessage: 'from {count} {count, plural, one {project} other {projects}}',
},
}) })
const auth = await useAuth() const auth = await useAuth()

View File

@@ -3,22 +3,31 @@
<section class="universal-card"> <section class="universal-card">
<Breadcrumbs <Breadcrumbs
v-if="history" v-if="history"
current-title="History" :current-title="formatMessage(messages.historyLabel)"
:link-stack="[{ href: `/dashboard/notifications`, label: 'Notifications' }]" :link-stack="[
{
href: `/dashboard/notifications`,
label: formatMessage(commonMessages.notificationsLabel),
},
]"
/> />
<div class="header__row"> <div class="header__row">
<div class="header__title"> <div class="header__title">
<h2 v-if="history" class="text-2xl">Notification history</h2> <h2 v-if="history" class="text-2xl">
<h2 v-else class="text-2xl">Notifications</h2> {{ formatMessage(messages.notificationHistoryTitle) }}
</h2>
<h2 v-else class="text-2xl">
{{ formatMessage(commonMessages.notificationsLabel) }}
</h2>
</div> </div>
<template v-if="!history"> <template v-if="!history">
<Button v-if="data.hasRead" @click="updateRoute()"> <Button v-if="data.hasRead" @click="updateRoute()">
<HistoryIcon /> <HistoryIcon />
View history {{ formatMessage(messages.viewHistory) }}
</Button> </Button>
<Button v-if="notifications.length > 0" color="danger" @click="readAll()"> <Button v-if="notifications.length > 0" color="danger" @click="readAll()">
<CheckCheckIcon /> <CheckCheckIcon />
Mark all as read {{ formatMessage(messages.markAllAsRead) }}
</Button> </Button>
</template> </template>
</div> </div>
@@ -29,9 +38,9 @@
:format-label="(x) => (x === 'all' ? 'All' : formatProjectType(x).replace('_', ' ') + 's')" :format-label="(x) => (x === 'all' ? 'All' : formatProjectType(x).replace('_', ' ') + 's')"
:capitalize="false" :capitalize="false"
/> />
<p v-if="isPending">Loading notifications...</p> <p v-if="isPending">{{ formatMessage(messages.loadingNotifications) }}</p>
<template v-else-if="error"> <template v-else-if="error">
<p>Error loading notifications:</p> <p>{{ formatMessage(messages.errorLoadingNotifications) }}</p>
<pre> <pre>
{{ error }} {{ error }}
</pre> </pre>
@@ -48,7 +57,7 @@
@update:notifications="() => refetch()" @update:notifications="() => refetch()"
/> />
</template> </template>
<p v-else>You don't have any unread notifications.</p> <p v-else>{{ formatMessage(messages.noUnreadNotifications) }}</p>
<div class="flex justify-end"> <div class="flex justify-end">
<Pagination :page="page" :count="pages" @switch-page="changePage" /> <Pagination :page="page" :count="pages" @switch-page="changePage" />
</div> </div>
@@ -57,7 +66,15 @@
</template> </template>
<script setup> <script setup>
import { CheckCheckIcon, HistoryIcon } from '@modrinth/assets' import { CheckCheckIcon, HistoryIcon } from '@modrinth/assets'
import { Button, Chips, injectModrinthClient, Pagination } from '@modrinth/ui' import {
Button,
Chips,
commonMessages,
defineMessages,
injectModrinthClient,
Pagination,
useVIntl,
} from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils' import { formatProjectType } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query' import { useQuery } from '@tanstack/vue-query'
@@ -69,8 +86,37 @@ import {
markAsRead, markAsRead,
} from '~/helpers/platform-notifications.ts' } from '~/helpers/platform-notifications.ts'
useHead({ const { formatMessage } = useVIntl()
title: 'Notifications - Modrinth',
const messages = defineMessages({
historyLabel: {
id: 'dashboard.notifications.history.label',
defaultMessage: 'History',
},
notificationHistoryTitle: {
id: 'dashboard.notifications.history.title',
defaultMessage: 'Notification history',
},
viewHistory: {
id: 'dashboard.notifications.button.view-history',
defaultMessage: 'View history',
},
markAllAsRead: {
id: 'dashboard.notifications.button.mark-all-as-read',
defaultMessage: 'Mark all as read',
},
loadingNotifications: {
id: 'dashboard.notifications.loading',
defaultMessage: 'Loading notifications...',
},
errorLoadingNotifications: {
id: 'dashboard.notifications.error.loading',
defaultMessage: 'Error loading notifications:',
},
noUnreadNotifications: {
id: 'dashboard.notifications.empty.no-unread',
defaultMessage: "You don't have any unread notifications.",
},
}) })
const client = injectModrinthClient() const client = injectModrinthClient()
@@ -79,6 +125,12 @@ const route = useNativeRoute()
const router = useNativeRouter() const router = useNativeRouter()
const history = computed(() => route.name === 'dashboard-notifications-history') const history = computed(() => route.name === 'dashboard-notifications-history')
useHead({
title: () =>
`${formatMessage(history.value ? messages.notificationHistoryTitle : commonMessages.notificationsLabel)} - Modrinth`,
})
const selectedType = ref('all') const selectedType = ref('all')
const page = ref(1) const page = ref(1)
const perPage = ref(50) const perPage = ref(50)

View File

@@ -3,11 +3,11 @@
<OrganizationCreateModal ref="createOrgModal" /> <OrganizationCreateModal ref="createOrgModal" />
<section class="universal-card"> <section class="universal-card">
<div class="header__row"> <div class="header__row">
<h2 class="header__title text-2xl">Organizations</h2> <h2 class="header__title text-2xl">{{ formatMessage(messages.organizationsTitle) }}</h2>
<div class="input-group"> <div class="input-group">
<button class="iconified-button brand-button" @click="openCreateOrgModal"> <button class="iconified-button brand-button" @click="openCreateOrgModal">
<PlusIcon aria-hidden="true" /> <PlusIcon aria-hidden="true" />
Create organization {{ formatMessage(messages.createOrganization) }}
</button> </button>
</div> </div>
</div> </div>
@@ -32,10 +32,11 @@
<div class="stats"> <div class="stats">
<UsersIcon aria-hidden="true" /> <UsersIcon aria-hidden="true" />
<span> <span>
{{ onlyAcceptedMembers(org.members).length }} {{
member<template v-if="onlyAcceptedMembers(org.members).length !== 1" formatMessage(messages.memberCount, {
>s</template count: onlyAcceptedMembers(org.members).length,
> })
}}
</span> </span>
</div> </div>
</span> </span>
@@ -43,19 +44,44 @@
</nuxt-link> </nuxt-link>
</div> </div>
</template> </template>
<template v-else> Make an organization! </template> <template v-else> {{ formatMessage(messages.makeOrganization) }} </template>
</section> </section>
</div> </div>
</template> </template>
<script setup> <script setup>
import { PlusIcon, UsersIcon } from '@modrinth/assets' import { PlusIcon, UsersIcon } from '@modrinth/assets'
import { Avatar, injectModrinthClient } from '@modrinth/ui' import { Avatar, defineMessages, injectModrinthClient, useVIntl } from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query' import { useQuery } from '@tanstack/vue-query'
import OrganizationCreateModal from '~/components/ui/create/OrganizationCreateModal.vue' import OrganizationCreateModal from '~/components/ui/create/OrganizationCreateModal.vue'
import { useAuth } from '~/composables/auth.js' import { useAuth } from '~/composables/auth.js'
const { formatMessage } = useVIntl()
const messages = defineMessages({
organizationsTitle: {
id: 'dashboard.organizations.title',
defaultMessage: 'Organizations',
},
createOrganization: {
id: 'dashboard.organizations.button.create',
defaultMessage: 'Create organization',
},
memberCount: {
id: 'dashboard.organizations.member-count',
defaultMessage: '{count} {count, plural, one {member} other {members}}',
},
makeOrganization: {
id: 'dashboard.organizations.empty.cta',
defaultMessage: 'Make an organization!',
},
fetchOrganizationsFailed: {
id: 'dashboard.organizations.error.fetch',
defaultMessage: 'Failed to fetch organizations',
},
})
const createOrgModal = ref(null) const createOrgModal = ref(null)
const auth = await useAuth() const auth = await useAuth()
@@ -77,7 +103,7 @@ const onlyAcceptedMembers = (members) => members.filter((member) => member?.acce
if (error.value) { if (error.value) {
createError({ createError({
statusCode: 500, statusCode: 500,
message: 'Failed to fetch organizations', message: formatMessage(messages.fetchOrganizationsFailed),
}) })
} }

View File

@@ -1,18 +1,11 @@
<template> <template>
<div> <div>
<NewModal ref="editLinksModal" header="Edit links"> <NewModal ref="editLinksModal" :header="formatMessage(messages.editLinksButton)">
<div class="universal-modal links-modal !p-0"> <div class="universal-modal links-modal !p-0">
<p> <p>{{ formatMessage(messages.editLinksDescription) }}</p>
Any links you specify below will be overwritten on each of the selected projects. Any you
leave blank will be ignored. You can clear a link from all selected projects using the
trash can button.
</p>
<section class="links"> <section class="links">
<label <label for="issue-tracker-input" :title="formatMessage(messages.issueTrackerDescription)">
for="issue-tracker-input" <span class="label__title">{{ formatMessage(messages.issueTrackerLabel) }}</span>
title="A place for users to report bugs, issues, and concerns about your project."
>
<span class="label__title">Issue tracker</span>
</label> </label>
<div class="input-group shrink-first"> <div class="input-group shrink-first">
<StyledInput <StyledInput
@@ -20,14 +13,12 @@
v-model="editLinks.issues.val" v-model="editLinks.issues.val"
:disabled="editLinks.issues.clear" :disabled="editLinks.issues.clear"
type="url" type="url"
:placeholder=" :placeholder="getLinkInputPlaceholder(editLinks.issues.clear)"
editLinks.issues.clear ? 'Existing link will be cleared' : 'Enter a valid URL'
"
:maxlength="2048" :maxlength="2048"
/> />
<button <button
v-tooltip="'Clear link'" v-tooltip="formatMessage(messages.clearLinkLabel)"
aria-label="Clear link" :aria-label="formatMessage(messages.clearLinkLabel)"
class="square-button label-button" class="square-button label-button"
:data-active="editLinks.issues.clear" :data-active="editLinks.issues.clear"
@click="editLinks.issues.clear = !editLinks.issues.clear" @click="editLinks.issues.clear = !editLinks.issues.clear"
@@ -35,11 +26,8 @@
<TrashIcon /> <TrashIcon />
</button> </button>
</div> </div>
<label <label for="source-code-input" :title="formatMessage(messages.sourceCodeDescription)">
for="source-code-input" <span class="label__title">{{ formatMessage(messages.sourceCodeLabel) }}</span>
title="A page/repository containing the source code for your project"
>
<span class="label__title">Source code</span>
</label> </label>
<div class="input-group shrink-first"> <div class="input-group shrink-first">
<StyledInput <StyledInput
@@ -48,13 +36,11 @@
:disabled="editLinks.source.clear" :disabled="editLinks.source.clear"
type="url" type="url"
:maxlength="2048" :maxlength="2048"
:placeholder=" :placeholder="getLinkInputPlaceholder(editLinks.source.clear)"
editLinks.source.clear ? 'Existing link will be cleared' : 'Enter a valid URL'
"
/> />
<button <button
v-tooltip="'Clear link'" v-tooltip="formatMessage(messages.clearLinkLabel)"
aria-label="Clear link" :aria-label="formatMessage(messages.clearLinkLabel)"
class="square-button label-button" class="square-button label-button"
:data-active="editLinks.source.clear" :data-active="editLinks.source.clear"
@click="editLinks.source.clear = !editLinks.source.clear" @click="editLinks.source.clear = !editLinks.source.clear"
@@ -62,11 +48,8 @@
<TrashIcon /> <TrashIcon />
</button> </button>
</div> </div>
<label <label for="wiki-page-input" :title="formatMessage(messages.wikiPageDescription)">
for="wiki-page-input" <span class="label__title">{{ formatMessage(messages.wikiPageLabel) }}</span>
title="A page containing information, documentation, and help for the project."
>
<span class="label__title">Wiki page</span>
</label> </label>
<div class="input-group shrink-first"> <div class="input-group shrink-first">
<StyledInput <StyledInput
@@ -75,13 +58,11 @@
:disabled="editLinks.wiki.clear" :disabled="editLinks.wiki.clear"
type="url" type="url"
:maxlength="2048" :maxlength="2048"
:placeholder=" :placeholder="getLinkInputPlaceholder(editLinks.wiki.clear)"
editLinks.wiki.clear ? 'Existing link will be cleared' : 'Enter a valid URL'
"
/> />
<button <button
v-tooltip="'Clear link'" v-tooltip="formatMessage(messages.clearLinkLabel)"
aria-label="Clear link" :aria-label="formatMessage(messages.clearLinkLabel)"
class="square-button label-button" class="square-button label-button"
:data-active="editLinks.wiki.clear" :data-active="editLinks.wiki.clear"
@click="editLinks.wiki.clear = !editLinks.wiki.clear" @click="editLinks.wiki.clear = !editLinks.wiki.clear"
@@ -89,8 +70,11 @@
<TrashIcon /> <TrashIcon />
</button> </button>
</div> </div>
<label for="discord-invite-input" title="An invitation link to your Discord server."> <label
<span class="label__title">Discord invite</span> for="discord-invite-input"
:title="formatMessage(messages.discordInviteDescription)"
>
<span class="label__title">{{ formatMessage(messages.discordInviteLabel) }}</span>
</label> </label>
<div class="input-group shrink-first"> <div class="input-group shrink-first">
<StyledInput <StyledInput
@@ -99,15 +83,11 @@
:disabled="editLinks.discord.clear" :disabled="editLinks.discord.clear"
type="url" type="url"
:maxlength="2048" :maxlength="2048"
:placeholder=" :placeholder="getLinkInputPlaceholder(editLinks.discord.clear, true)"
editLinks.discord.clear
? 'Existing link will be cleared'
: 'Enter a valid Discord invite URL'
"
/> />
<button <button
v-tooltip="'Clear link'" v-tooltip="formatMessage(messages.clearLinkLabel)"
aria-label="Clear link" :aria-label="formatMessage(messages.clearLinkLabel)"
class="square-button label-button" class="square-button label-button"
:data-active="editLinks.discord.clear" :data-active="editLinks.discord.clear"
@click="editLinks.discord.clear = !editLinks.discord.clear" @click="editLinks.discord.clear = !editLinks.discord.clear"
@@ -117,10 +97,14 @@
</div> </div>
</section> </section>
<p> <p>
Changes will be applied to <IntlFormatted
<strong>{{ selectedProjects.length }}</strong> project{{ :message-id="messages.changesAppliedTo"
selectedProjects.length > 1 ? 's' : '' :values="{ count: selectedProjects.length }"
}}. >
<template #strong="{ children }">
<strong><component :is="() => children" /></strong>
</template>
</IntlFormatted>
</p> </p>
<ul> <ul>
<li <li
@@ -133,23 +117,25 @@
{{ project.title }} {{ project.title }}
</li> </li>
<li v-if="!editLinks.showAffected && selectedProjects.length > 3"> <li v-if="!editLinks.showAffected && selectedProjects.length > 3">
<strong>and {{ selectedProjects.length - 3 }} more...</strong> <strong>{{
formatMessage(messages.andMore, { count: selectedProjects.length - 3 })
}}</strong>
</li> </li>
</ul> </ul>
<Checkbox <Checkbox
v-if="selectedProjects.length > 3" v-if="selectedProjects.length > 3"
v-model="editLinks.showAffected" v-model="editLinks.showAffected"
label="Show all projects" :label="formatMessage(messages.showAllProjects)"
description="Show all projects" :description="formatMessage(messages.showAllProjects)"
/> />
<div class="push-right input-group"> <div class="push-right input-group">
<button class="iconified-button" @click="$refs.editLinksModal.hide()"> <button class="iconified-button" @click="$refs.editLinksModal.hide()">
<XIcon /> <XIcon />
Cancel {{ formatMessage(commonMessages.cancelButton) }}
</button> </button>
<button class="iconified-button brand-button" @click="bulkEditLinks()"> <button class="iconified-button brand-button" @click="bulkEditLinks()">
<SaveIcon /> <SaveIcon />
Save changes {{ formatMessage(commonMessages.saveChangesButton) }}
</button> </button>
</div> </div>
</div> </div>
@@ -157,7 +143,7 @@
<ModalCreation ref="modal_creation" /> <ModalCreation ref="modal_creation" />
<section class="universal-card"> <section class="universal-card">
<div class="header__row"> <div class="header__row">
<h2 class="header__title text-2xl">Projects</h2> <h2 class="header__title text-2xl">{{ formatMessage(messages.headTitle) }}</h2>
<div class="input-group"> <div class="input-group">
<button class="iconified-button brand-button" @click="$refs.modal_creation.show($event)"> <button class="iconified-button brand-button" @click="$refs.modal_creation.show($event)">
<PlusIcon /> <PlusIcon />
@@ -166,10 +152,10 @@
</div> </div>
</div> </div>
<p v-if="projects.length < 1"> <p v-if="projects.length < 1">
You don't have any projects yet. Click the green button above to begin. {{ formatMessage(messages.noProjectsYet) }}
</p> </p>
<template v-else> <template v-else>
<p>You can edit multiple projects at once by selecting them below.</p> <p>{{ formatMessage(messages.bulkEditHint) }}</p>
<div class="input-group"> <div class="input-group">
<button <button
class="iconified-button" class="iconified-button"
@@ -177,11 +163,11 @@
@click="$refs.editLinksModal.show()" @click="$refs.editLinksModal.show()"
> >
<EditIcon /> <EditIcon />
Edit links {{ formatMessage(messages.editLinksButton) }}
</button> </button>
<div class="push-right"> <div class="push-right">
<div class="labeled-control-row"> <div class="labeled-control-row">
Sort by {{ formatMessage(commonMessages.sortByLabel) }}
<Combobox <Combobox
v-model="sortBy" v-model="sortBy"
:searchable="false" :searchable="false"
@@ -190,7 +176,7 @@
@update:model-value="projects = updateSort(projects, sortBy, descending)" @update:model-value="projects = updateSort(projects, sortBy, descending)"
/> />
<button <button
v-tooltip="descending ? 'Descending' : 'Ascending'" v-tooltip="formatMessage(descending ? messages.descending : messages.ascending)"
class="square-button" class="square-button"
@click="updateDescending()" @click="updateDescending()"
> >
@@ -208,11 +194,11 @@
@update:model-value="toggleAllBulkEditableProjects()" @update:model-value="toggleAllBulkEditableProjects()"
/> />
</div> </div>
<div>Icon</div> <div>{{ formatMessage(messages.iconHeader) }}</div>
<div>Name</div> <div>{{ formatMessage(messages.nameHeader) }}</div>
<div>ID</div> <div>{{ formatMessage(messages.idHeader) }}</div>
<div>Type</div> <div>{{ formatMessage(messages.typeHeader) }}</div>
<div>Status</div> <div>{{ formatMessage(messages.statusHeader) }}</div>
<div /> <div />
</div> </div>
<div v-for="project in projects" :key="`project-${project.id}`" class="grid-table__row"> <div v-for="project in projects" :key="`project-${project.id}`" class="grid-table__row">
@@ -234,7 +220,7 @@
<Avatar <Avatar
:src="project.icon_url" :src="project.icon_url"
aria-hidden="true" aria-hidden="true"
:alt="'Icon for ' + project.title" :alt="formatMessage(messages.projectIconAlt, { title: project.title })"
no-shadow no-shadow
/> />
</nuxt-link> </nuxt-link>
@@ -244,7 +230,7 @@
<span class="project-title"> <span class="project-title">
<IssuesIcon <IssuesIcon
v-if="project.moderator_message" v-if="project.moderator_message"
aria-label="Project has a message from the moderators. View the project to see more." :aria-label="formatMessage(messages.projectModeratorMessageAriaLabel)"
/> />
<nuxt-link <nuxt-link
@@ -277,7 +263,7 @@
color="orange" color="orange"
> >
<nuxt-link <nuxt-link
v-tooltip="'Please review environment metadata'" v-tooltip="formatMessage(messages.reviewEnvironmentMetadata)"
:to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${ :to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${
project.slug ? project.slug : project.id project.slug ? project.slug : project.id
}?showEnvironmentMigrationWarning=true`" }?showEnvironmentMigrationWarning=true`"
@@ -323,7 +309,9 @@ import {
Combobox, Combobox,
commonMessages, commonMessages,
CopyCode, CopyCode,
defineMessages,
injectNotificationManager, injectNotificationManager,
IntlFormatted,
NewModal, NewModal,
ProjectStatusBadge, ProjectStatusBadge,
StyledInput, StyledInput,
@@ -334,8 +322,6 @@ import { formatProjectType } from '@modrinth/utils'
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue' import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
import { getProjectTypeForUrl } from '~/helpers/projects.js' import { getProjectTypeForUrl } from '~/helpers/projects.js'
useHead({ title: 'Projects - Modrinth' })
// const UPLOAD_VERSION = 1 << 0 // const UPLOAD_VERSION = 1 << 0
// const DELETE_VERSION = 1 << 1 // const DELETE_VERSION = 1 << 1
const EDIT_DETAILS = 1 << 2 const EDIT_DETAILS = 1 << 2
@@ -348,16 +334,163 @@ const EDIT_DETAILS = 1 << 2
const { addNotification } = injectNotificationManager() const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const messages = defineMessages({
headTitle: {
id: 'dashboard.projects.head-title',
defaultMessage: 'Projects',
},
editLinksButton: {
id: 'dashboard.projects.links.button.edit',
defaultMessage: 'Edit links',
},
editLinksDescription: {
id: 'dashboard.projects.links.description',
defaultMessage:
'Any links you specify below will be overwritten on each of the selected projects. Any you leave blank will be ignored. You can clear a link from all selected projects using the trash can button.',
},
issueTrackerLabel: {
id: 'dashboard.projects.links.issue-tracker.label',
defaultMessage: 'Issue tracker',
},
issueTrackerDescription: {
id: 'dashboard.projects.links.issue-tracker.description',
defaultMessage: 'A place for users to report bugs, issues, and concerns about your project.',
},
sourceCodeLabel: {
id: 'dashboard.projects.links.source-code.label',
defaultMessage: 'Source code',
},
sourceCodeDescription: {
id: 'dashboard.projects.links.source-code.description',
defaultMessage: 'A page/repository containing the source code for your project',
},
wikiPageLabel: {
id: 'dashboard.projects.links.wiki-page.label',
defaultMessage: 'Wiki page',
},
wikiPageDescription: {
id: 'dashboard.projects.links.wiki-page.description',
defaultMessage: 'A page containing information, documentation, and help for the project.',
},
discordInviteLabel: {
id: 'dashboard.projects.links.discord-invite.label',
defaultMessage: 'Discord invite',
},
discordInviteDescription: {
id: 'dashboard.projects.links.discord-invite.description',
defaultMessage: 'An invitation link to your Discord server.',
},
existingLinkWillBeCleared: {
id: 'dashboard.projects.links.placeholder.cleared',
defaultMessage: 'Existing link will be cleared',
},
enterValidUrl: {
id: 'dashboard.projects.links.placeholder.valid-url',
defaultMessage: 'Enter a valid URL',
},
enterValidDiscordInviteUrl: {
id: 'dashboard.projects.links.placeholder.valid-discord-url',
defaultMessage: 'Enter a valid Discord invite URL',
},
clearLinkLabel: {
id: 'dashboard.projects.links.button.clear-link',
defaultMessage: 'Clear link',
},
changesAppliedTo: {
id: 'dashboard.projects.links.changes-applied',
defaultMessage:
'Changes will be applied to <strong>{count}</strong> {count, plural, one {project} other {projects}}.',
},
andMore: {
id: 'dashboard.projects.links.and-more',
defaultMessage: 'and {count} more...',
},
showAllProjects: {
id: 'dashboard.projects.links.show-all-projects',
defaultMessage: 'Show all projects',
},
noProjectsYet: {
id: 'dashboard.projects.empty',
defaultMessage: "You don't have any projects yet. Click the green button above to begin.",
},
bulkEditHint: {
id: 'dashboard.projects.bulk-edit-hint',
defaultMessage: 'You can edit multiple projects at once by selecting them below.',
},
ascending: {
id: 'dashboard.projects.sort.ascending',
defaultMessage: 'Ascending',
},
descending: {
id: 'dashboard.projects.sort.descending',
defaultMessage: 'Descending',
},
sortOptionName: {
id: 'dashboard.projects.sort.option.name',
defaultMessage: 'Name',
},
sortOptionStatus: {
id: 'dashboard.projects.sort.option.status',
defaultMessage: 'Status',
},
sortOptionType: {
id: 'dashboard.projects.sort.option.type',
defaultMessage: 'Type',
},
iconHeader: {
id: 'dashboard.projects.table.icon',
defaultMessage: 'Icon',
},
nameHeader: {
id: 'dashboard.projects.table.name',
defaultMessage: 'Name',
},
idHeader: {
id: 'dashboard.projects.table.id',
defaultMessage: 'ID',
},
typeHeader: {
id: 'dashboard.projects.table.type',
defaultMessage: 'Type',
},
statusHeader: {
id: 'dashboard.projects.table.status',
defaultMessage: 'Status',
},
projectIconAlt: {
id: 'dashboard.projects.project.icon-alt',
defaultMessage: 'Icon for {title}',
},
projectModeratorMessageAriaLabel: {
id: 'dashboard.projects.project.moderator-message-aria',
defaultMessage: 'Project has a message from the moderators. View the project to see more.',
},
reviewEnvironmentMetadata: {
id: 'dashboard.projects.project.review-environment-metadata',
defaultMessage: 'Please review environment metadata',
},
serverBulkEditDisabled: {
id: 'dashboard.projects.bulk-edit.server-disabled',
defaultMessage: 'Server projects do not support bulk editing',
},
bulkEditSuccessText: {
id: 'dashboard.projects.notification.bulk-edit-success',
defaultMessage: "Bulk edited selected project's links.",
},
})
useHead({ title: () => `${formatMessage(messages.headTitle)} - Modrinth` })
const user = await useUser() const user = await useUser()
const projects = ref([]) const projects = ref([])
const projectsWithMigrationWarning = ref([]) const projectsWithMigrationWarning = ref([])
const selectedProjects = ref([]) const selectedProjects = ref([])
const sortBy = ref('Name') const sortBy = ref('Name')
const sortOptions = [ const sortOptions = computed(() => [
{ value: 'Name', label: 'Name' }, { value: 'Name', label: formatMessage(messages.sortOptionName) },
{ value: 'Status', label: 'Status' }, { value: 'Status', label: formatMessage(messages.sortOptionStatus) },
{ value: 'Type', label: 'Type' }, { value: 'Type', label: formatMessage(messages.sortOptionType) },
] ])
const descending = ref(false) const descending = ref(false)
const editLinks = reactive({ const editLinks = reactive({
showAffected: false, showAffected: false,
@@ -370,6 +503,16 @@ const editLinks = reactive({
const editLinksModal = ref(null) const editLinksModal = ref(null)
const modal_creation = ref(null) const modal_creation = ref(null)
function getLinkInputPlaceholder(clearLink, isDiscord = false) {
if (clearLink) {
return formatMessage(messages.existingLinkWillBeCleared)
}
return isDiscord
? formatMessage(messages.enterValidDiscordInviteUrl)
: formatMessage(messages.enterValidUrl)
}
function isProjectBulkEditDisabled(project) { function isProjectBulkEditDisabled(project) {
return ( return (
(project.permissions & EDIT_DETAILS) === EDIT_DETAILS || (project.permissions & EDIT_DETAILS) === EDIT_DETAILS ||
@@ -408,7 +551,7 @@ function toggleProjectSelection(project) {
function getBulkEditDisabledTooltip(project) { function getBulkEditDisabledTooltip(project) {
if (project.project_type === 'minecraft_java_server') { if (project.project_type === 'minecraft_java_server') {
return 'Server projects do not support bulk editing' return formatMessage(messages.serverBulkEditDisabled)
} }
return '' return ''
@@ -467,8 +610,8 @@ async function bulkEditLinks() {
editLinksModal.value?.hide() editLinksModal.value?.hide()
addNotification({ addNotification({
title: 'Success', title: formatMessage(commonMessages.successLabel),
text: "Bulk edited selected project's links.", text: formatMessage(messages.bulkEditSuccessText),
type: 'success', type: 'success',
}) })
selectedProjects.value = [] selectedProjects.value = []
@@ -483,7 +626,7 @@ async function bulkEditLinks() {
editLinks.discord.clear = false editLinks.discord.clear = false
} catch (e) { } catch (e) {
addNotification({ addNotification({
title: 'An error occurred', title: formatMessage(commonMessages.errorNotificationTitle),
text: e, text: e,
type: 'error', type: 'error',
}) })

View File

@@ -2,16 +2,33 @@
<ReportView <ReportView
:auth="auth" :auth="auth"
:report-id="route.params.id" :report-id="route.params.id"
:breadcrumbs-stack="[{ href: '/dashboard/reports', label: 'Active reports' }]" :breadcrumbs-stack="[
{ href: '/dashboard/reports', label: formatMessage(messages.activeReportsTitle) },
]"
/> />
</template> </template>
<script setup> <script setup>
import { defineMessages, useVIntl } from '@modrinth/ui'
import ReportView from '~/components/ui/report/ReportView.vue' import ReportView from '~/components/ui/report/ReportView.vue'
const { formatMessage } = useVIntl()
const messages = defineMessages({
activeReportsTitle: {
id: 'dashboard.reports.active-title',
defaultMessage: 'Active reports',
},
reportTitle: {
id: 'dashboard.report.title',
defaultMessage: 'Report {id}',
},
})
const route = useNativeRoute() const route = useNativeRoute()
const auth = await useAuth() const auth = await useAuth()
useHead({ useHead({
title: `Report ${route.params.id} - Modrinth`, title: () => `${formatMessage(messages.reportTitle, { id: route.params.id })} - Modrinth`,
}) })
</script> </script>

View File

@@ -1,16 +1,31 @@
<template> <template>
<div> <div>
<section class="universal-card"> <section class="universal-card">
<h2 class="text-2xl">Reports</h2> <h2 class="text-2xl">{{ formatMessage(messages.reportsTitle) }}</h2>
<ReportsList :auth="auth" /> <ReportsList :auth="auth" />
</section> </section>
</div> </div>
</template> </template>
<script setup> <script setup>
import { defineMessages, useVIntl } from '@modrinth/ui'
import ReportsList from '~/components/ui/report/ReportsList.vue' import ReportsList from '~/components/ui/report/ReportsList.vue'
const { formatMessage } = useVIntl()
const messages = defineMessages({
reportsTitle: {
id: 'dashboard.reports.title',
defaultMessage: 'Reports',
},
activeReportsTitle: {
id: 'dashboard.reports.active-title',
defaultMessage: 'Active reports',
},
})
const auth = await useAuth() const auth = await useAuth()
useHead({ useHead({
title: 'Active reports - Modrinth', title: () => `${formatMessage(messages.activeReportsTitle)} - Modrinth`,
}) })
</script> </script>

View File

@@ -298,8 +298,14 @@ async function openWithdrawModal() {
} }
const messages = defineMessages({ const messages = defineMessages({
balanceLabel: { id: 'dashboard.revenue.balance', defaultMessage: 'Balance' }, balanceLabel: {
availableNow: { id: 'dashboard.revenue.available-now', defaultMessage: 'Available now' }, id: 'dashboard.revenue.balance',
defaultMessage: 'Balance',
},
availableNow: {
id: 'dashboard.revenue.available-now',
defaultMessage: 'Available now',
},
estimatedWithDate: { estimatedWithDate: {
id: 'dashboard.revenue.estimated-with-date', id: 'dashboard.revenue.estimated-with-date',
defaultMessage: 'Estimated {date}', defaultMessage: 'Estimated {date}',
@@ -312,14 +318,23 @@ const messages = defineMessages({
id: 'dashboard.revenue.estimated-tooltip.msg2', id: 'dashboard.revenue.estimated-tooltip.msg2',
defaultMessage: 'Click to read about how Modrinth handles your revenue.', defaultMessage: 'Click to read about how Modrinth handles your revenue.',
}, },
processing: { id: 'dashboard.revenue.processing', defaultMessage: 'Processing' }, processing: {
id: 'dashboard.revenue.processing',
defaultMessage: 'Processing',
},
processingTooltip: { processingTooltip: {
id: 'dashboard.revenue.processing.tooltip', id: 'dashboard.revenue.processing.tooltip',
defaultMessage: defaultMessage:
'Revenue stays in processing until the end of the month, then becomes available 60 days later.', 'Revenue stays in processing until the end of the month, then becomes available 60 days later.',
}, },
withdrawHeader: { id: 'dashboard.revenue.withdraw.header', defaultMessage: 'Withdraw' }, withdrawHeader: {
withdrawCardTitle: { id: 'dashboard.revenue.withdraw.card.title', defaultMessage: 'Withdraw' }, id: 'dashboard.revenue.withdraw.header',
defaultMessage: 'Withdraw',
},
withdrawCardTitle: {
id: 'dashboard.revenue.withdraw.card.title',
defaultMessage: 'Withdraw',
},
withdrawCardDescription: { withdrawCardDescription: {
id: 'dashboard.revenue.withdraw.card.description', id: 'dashboard.revenue.withdraw.card.description',
defaultMessage: 'Withdraw from your available balance to any payout method.', defaultMessage: 'Withdraw from your available balance to any payout method.',
@@ -338,7 +353,10 @@ const messages = defineMessages({
id: 'dashboard.revenue.transactions.header', id: 'dashboard.revenue.transactions.header',
defaultMessage: 'Transactions', defaultMessage: 'Transactions',
}, },
seeAll: { id: 'dashboard.revenue.transactions.see-all', defaultMessage: 'See all' }, seeAll: {
id: 'dashboard.revenue.transactions.see-all',
defaultMessage: 'See all',
},
noTransactions: { noTransactions: {
id: 'dashboard.revenue.transactions.none', id: 'dashboard.revenue.transactions.none',
defaultMessage: 'No transactions', defaultMessage: 'No transactions',

View File

@@ -8,7 +8,9 @@
<Combobox <Combobox
v-model="selectedYear" v-model="selectedYear"
:options="yearOptions" :options="yearOptions"
:display-value="selectedYear === 'all' ? 'All years' : String(selectedYear)" :display-value="
selectedYear === 'all' ? formatMessage(messages.allYears) : String(selectedYear)
"
listbox listbox
/> />
<ButtonStyled circular> <ButtonStyled circular>
@@ -116,7 +118,7 @@ const client = injectModrinthClient()
const generatedState = useGeneratedState() const generatedState = useGeneratedState()
useHead({ useHead({
title: 'Transaction history - Modrinth', title: () => `${formatMessage(messages.headTitle)} - Modrinth`,
}) })
const { data: transactions, refetch } = useQuery({ const { data: transactions, refetch } = useQuery({
@@ -143,7 +145,7 @@ const yearOptions = computed(() => {
return yearValues.map((year) => ({ return yearValues.map((year) => ({
value: year, value: year,
label: year === 'all' ? 'All years' : String(year), label: year === 'all' ? formatMessage(messages.allYears) : String(year),
})) }))
}) })
@@ -161,9 +163,9 @@ function getPeriodLabel(date) {
const now = dayjs() const now = dayjs()
if (txnDate.isSame(now, 'month')) { if (txnDate.isSame(now, 'month')) {
return 'This month' return formatMessage(messages.thisMonth)
} else if (txnDate.isSame(now.subtract(1, 'month'), 'month')) { } else if (txnDate.isSame(now.subtract(1, 'month'), 'month')) {
return 'Last month' return formatMessage(messages.lastMonth)
} else { } else {
return capitalizeString(formatMonth(txnDate.toDate())) return capitalizeString(formatMonth(txnDate.toDate()))
} }
@@ -322,6 +324,10 @@ const messages = defineMessages({
id: 'dashboard.revenue.transactions.header', id: 'dashboard.revenue.transactions.header',
defaultMessage: 'Transactions', defaultMessage: 'Transactions',
}, },
headTitle: {
id: 'dashboard.revenue.transactions.head-title',
defaultMessage: 'Transaction history',
},
received: { received: {
id: 'dashboard.revenue.stats.received', id: 'dashboard.revenue.stats.received',
defaultMessage: 'Received', defaultMessage: 'Received',
@@ -346,5 +352,17 @@ const messages = defineMessages({
id: 'dashboard.revenue.transactions.btn.download-csv', id: 'dashboard.revenue.transactions.btn.download-csv',
defaultMessage: 'Download as CSV', defaultMessage: 'Download as CSV',
}, },
allYears: {
id: 'dashboard.revenue.transactions.year.all',
defaultMessage: 'All years',
},
thisMonth: {
id: 'dashboard.revenue.transactions.period.this-month',
defaultMessage: 'This month',
},
lastMonth: {
id: 'dashboard.revenue.transactions.period.last-month',
defaultMessage: 'Last month',
},
}) })
</script> </script>