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": {
"message": "Create affiliate link"
},
"dashboard.affiliate-links.empty.no-codes": {
"message": "No affiliate codes found."
},
"dashboard.affiliate-links.error.title": {
"message": "Error loading affiliate links"
},
@@ -572,6 +575,15 @@
"dashboard.affiliate-links.search": {
"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": {
"message": "Create new"
},
@@ -596,6 +608,18 @@
"dashboard.collections.long-title": {
"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": {
"message": "Download {formType}"
},
@@ -848,6 +872,168 @@
"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."
},
"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": {
"message": "Available now"
},
@@ -884,6 +1070,9 @@
"dashboard.revenue.transactions.btn.download-csv": {
"message": "Download as CSV"
},
"dashboard.revenue.transactions.head-title": {
"message": "Transaction history"
},
"dashboard.revenue.transactions.header": {
"message": "Transactions"
},
@@ -893,9 +1082,18 @@
"dashboard.revenue.transactions.none.desc": {
"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": {
"message": "See all"
},
"dashboard.revenue.transactions.year.all": {
"message": "All years"
},
"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."
},
@@ -908,6 +1106,33 @@
"dashboard.revenue.withdraw.header": {
"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": {
"message": "Account"
},

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@
:icon="SearchIcon"
type="text"
clearable
placeholder="Search collections..."
:placeholder="formatMessage(messages.searchCollectionsPlaceholder)"
wrapper-class="w-full"
input-class="!h-12"
/>
@@ -20,18 +20,13 @@
v-slot="{ selected }"
v-model="sortBy"
class="!w-auto flex-grow md:flex-grow-0"
name="Sort by"
:name="formatMessage(commonMessages.sortByLabel)"
:options="['updated', 'created', 'name']"
:display-name="
(option) =>
option === 'updated'
? 'Recently Updated'
: option === 'created'
? 'Recently Created'
: 'Name (A-Z)'
"
:display-name="formatCollectionSortOption"
>
<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>
</DropdownSelect>
@@ -188,6 +183,22 @@ const messages = defineMessages({
id: 'dashboard.collections.label.search-input',
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: {
id: 'dashboard.collections.empty.no-match',
defaultMessage: 'No collections match your search',
@@ -234,6 +245,18 @@ const router = useNativeRouter()
const validSortOptions = ['updated', 'created', 'name']
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(() => {
if (!collections.value) return []
return [...collections.value]

View File

@@ -7,7 +7,7 @@
{{ auth.user.username }}
</h1>
<NuxtLink class="goto-link" :to="`/user/${auth.user.username}`">
Visit your profile
{{ formatMessage(commonMessages.visitYourProfile) }}
<ChevronRightIcon class="featured-header-chevron" aria-hidden="true" />
</NuxtLink>
</div>
@@ -15,13 +15,15 @@
<div class="dashboard-notifications">
<section class="universal-card">
<div class="header__row">
<h2 class="header__title text-2xl">Notifications</h2>
<h2 class="header__title text-2xl">
{{ formatMessage(commonMessages.notificationsLabel) }}
</h2>
<nuxt-link
v-if="notifications.length > 0"
class="goto-link"
to="/dashboard/notifications"
>
See all
{{ formatMessage(messages.seeAll) }}
<ChevronRightIcon />
</nuxt-link>
</div>
@@ -42,15 +44,15 @@
class="goto-link view-more-notifs mt-4"
to="/dashboard/notifications"
>
View {{ extraNotifs }} more notification{{ extraNotifs === 1 ? '' : 's' }}
{{ formatMessage(messages.viewMore, { extraNotifs: extraNotifs }) }}
<ChevronRightIcon />
</nuxt-link>
</template>
<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">
<HistoryIcon />
View notification history
{{ formatMessage(messages.viewNotificationHistory) }}
</nuxt-link>
</div>
</section>
@@ -58,18 +60,16 @@
<div class="dashboard-analytics">
<section class="universal-card">
<h2>Analytics</h2>
<h2>{{ formatMessage(commonMessages.analyticsButton) }}</h2>
<div class="grid-display">
<div class="grid-display__item">
<div class="label">Total downloads</div>
<div class="label">{{ formatMessage(messages.totalDownloads) }}</div>
<div class="value">
{{ $formatNumber(projects.reduce((agg, x) => agg + x.downloads, 0)) }}
</div>
<span
>from
{{ downloadsProjectCount }}
project{{ downloadsProjectCount === 1 ? '' : 's' }}</span
>
<span>{{
formatMessage(messages.fromProjects, { count: downloadsProjectCount })
}}</span>
<!-- <NuxtLink class="goto-link" to="/dashboard/analytics"-->
<!-- >View breakdown-->
<!-- <ChevronRightIcon-->
@@ -78,16 +78,15 @@
<!-- /></NuxtLink>-->
</div>
<div class="grid-display__item">
<div class="label">Total followers</div>
<div class="label">{{ formatMessage(messages.totalFollowers) }}</div>
<div class="value">
{{ $formatNumber(projects.reduce((agg, x) => agg + x.followers, 0)) }}
</div>
<span>
<span
>from {{ followersProjectCount }} project{{
followersProjectCount === 1 ? '' : 's'
}}</span
></span
<span>{{
formatMessage(messages.fromProjects, { count: followersProjectCount })
}}</span></span
>
>
</div>
</div>
@@ -97,14 +96,58 @@
</template>
<script setup>
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 NotificationItem from '~/components/ui/NotificationItem.vue'
import { fetchExtraNotificationData, groupNotifications } from '~/helpers/platform-notifications.ts'
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()

View File

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

View File

@@ -3,11 +3,11 @@
<OrganizationCreateModal ref="createOrgModal" />
<section class="universal-card">
<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">
<button class="iconified-button brand-button" @click="openCreateOrgModal">
<PlusIcon aria-hidden="true" />
Create organization
{{ formatMessage(messages.createOrganization) }}
</button>
</div>
</div>
@@ -32,10 +32,11 @@
<div class="stats">
<UsersIcon aria-hidden="true" />
<span>
{{ onlyAcceptedMembers(org.members).length }}
member<template v-if="onlyAcceptedMembers(org.members).length !== 1"
>s</template
>
{{
formatMessage(messages.memberCount, {
count: onlyAcceptedMembers(org.members).length,
})
}}
</span>
</div>
</span>
@@ -43,19 +44,44 @@
</nuxt-link>
</div>
</template>
<template v-else> Make an organization! </template>
<template v-else> {{ formatMessage(messages.makeOrganization) }} </template>
</section>
</div>
</template>
<script setup>
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 OrganizationCreateModal from '~/components/ui/create/OrganizationCreateModal.vue'
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 auth = await useAuth()
@@ -77,7 +103,7 @@ const onlyAcceptedMembers = (members) => members.filter((member) => member?.acce
if (error.value) {
createError({
statusCode: 500,
message: 'Failed to fetch organizations',
message: formatMessage(messages.fetchOrganizationsFailed),
})
}

View File

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

View File

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

View File

@@ -1,16 +1,31 @@
<template>
<div>
<section class="universal-card">
<h2 class="text-2xl">Reports</h2>
<h2 class="text-2xl">{{ formatMessage(messages.reportsTitle) }}</h2>
<ReportsList :auth="auth" />
</section>
</div>
</template>
<script setup>
import { defineMessages, useVIntl } from '@modrinth/ui'
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()
useHead({
title: 'Active reports - Modrinth',
title: () => `${formatMessage(messages.activeReportsTitle)} - Modrinth`,
})
</script>

View File

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

View File

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