Add translation keys for collections (#1496)

* Begin Work

* Add more common messages

* Work on modal

* Add more keys

* Add keys for icon buttons

* Add more keys

* Handle error keys

* Add more keys

* Add more keys

* Edit fields keys

* Finish (almost)

* Finish work for collection page

* Dashboard Nav stack & Format

* WIP

* Move some messages to common

* Finish work

* Format

* Reorganization

* Fix some mistake

* add common collections label

* Add collections label key to default layout

* Make title and description reactive (#8)

---------

Co-authored-by: Sasha Sorokin <10401817+brawaru@users.noreply.github.com>
This commit is contained in:
Mysterious_Dev 2024-01-27 18:24:19 +01:00 committed by GitHub
parent 0195e94aa7
commit 52b299315d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 375 additions and 67 deletions

View File

@ -99,7 +99,7 @@
<hr class="divider" />
<NuxtLink class="item button-transparent" to="/dashboard/collections">
<LibraryIcon class="icon" />
<span class="title">Collections</span>
<span class="title">{{ formatMessage(commonMessages.collectionsLabel) }}</span>
</NuxtLink>
<NuxtLink class="item button-transparent" to="/dashboard/notifications">
<NotificationIcon class="icon" />
@ -398,6 +398,8 @@ import NavRow from '~/components/ui/NavRow.vue'
import ModalCreation from '~/components/ui/ModalCreation.vue'
import Avatar from '~/components/ui/Avatar.vue'
const { formatMessage } = useVIntl()
const app = useNuxtApp()
const auth = await useAuth()
const cosmetics = useCosmetics()

View File

@ -26,6 +26,78 @@
"button.save": {
"message": "Save"
},
"collection.button.delete-icon": {
"message": "Delete icon"
},
"collection.button.edit-icon": {
"message": "Edit icon"
},
"collection.button.remove-project": {
"message": "Remove project"
},
"collection.button.unfollow-project": {
"message": "Unfollow project"
},
"collection.button.upload-icon": {
"message": "Upload icon"
},
"collection.delete-modal.description": {
"message": "This will remove this collection forever. This action cannot be undone."
},
"collection.delete-modal.title": {
"message": "Are you sure you want to delete this collection?"
},
"collection.description": {
"message": "{description} - View the collection {name} by {username} on Modrinth"
},
"collection.description.following": {
"message": "Auto-generated collection of all the projects you're following."
},
"collection.error.not-found": {
"message": "Collection not found"
},
"collection.label.collection": {
"message": "Collection"
},
"collection.label.created-at": {
"message": "Created {ago}"
},
"collection.label.curated-by": {
"message": "Curated by"
},
"collection.label.no-projects": {
"message": "This collection has no projects!"
},
"collection.label.no-projects-auth": {
"message": "You don't have any projects.\nWould you like to <create-link>add one</create-link>?"
},
"collection.label.owner": {
"message": "Owner"
},
"collection.label.private": {
"message": "Private"
},
"collection.label.projects-count": {
"message": "{count, plural, one {<stat>{count}</stat> project} other {<stat>{count}</stat> projects}}"
},
"collection.label.updated-at": {
"message": "Updated {ago}"
},
"collection.title": {
"message": "{name} - Collection"
},
"dashboard.collections.button.create-new": {
"message": "Create new"
},
"dashboard.collections.label.projects-count": {
"message": "{count, plural, one {{count} project} other {{count} projects}}"
},
"dashboard.collections.label.search-input": {
"message": "Search your collections"
},
"dashboard.collections.long-title": {
"message": "Your collections"
},
"frog": {
"message": "You've been frogged! 🐸"
},
@ -50,6 +122,33 @@
"input.view.list": {
"message": "List view"
},
"label.collections": {
"message": "Collections"
},
"label.delete": {
"message": "Delete"
},
"label.description": {
"message": "Description"
},
"label.followed-projects": {
"message": "Followed projects"
},
"label.public": {
"message": "Public"
},
"label.rejected": {
"message": "Rejected"
},
"label.title": {
"message": "Title"
},
"label.unlisted": {
"message": "Unlisted"
},
"label.visibility": {
"message": "Visibility"
},
"notification.error.title": {
"message": "An error occurred"
},

View File

@ -3,10 +3,10 @@
<ModalConfirm
v-if="auth.user && auth.user.id === creator.id"
ref="deleteModal"
title="Are you sure you want to delete this collection?"
description="This will remove this collection forever. This action cannot be undone."
:title="formatMessage(messages.deleteModalTitle)"
:description="formatMessage(messages.deleteModalDescription)"
:has-to-type="false"
proceed-label="Delete"
:proceed-label="formatMessage(commonMessages.deleteLabel)"
@proceed="deleteCollection()"
/>
<div class="normal-page">
@ -16,16 +16,16 @@
<template v-if="canEdit && isEditing === false">
<Button @click="isEditing = true">
<EditIcon />
Edit
{{ formatMessage(commonMessages.editButton) }}
</Button>
<Button id="delete-collection" @click="() => $refs.deleteModal.show()">
<TrashIcon />
Delete
{{ formatMessage(commonMessages.deleteLabel) }}
</Button>
</template>
<template v-else-if="canEdit && isEditing === true">
<PopoutMenu class="btn" position="bottom" direction="right">
<EditIcon /> Edit icon
<EditIcon /> {{ formatMessage(messages.editIconButton) }}
<template #menu>
<span class="icon-edit-menu">
<FileInput
@ -39,7 +39,7 @@
@change="showPreviewImage"
>
<UploadIcon />
Upload icon
{{ formatMessage(messages.uploadIconButton) }}
</FileInput>
<Button
v-if="!deletedIcon && (previewImage || collection.icon_url)"
@ -53,7 +53,7 @@
"
>
<TrashIcon />
Delete icon
{{ formatMessage(messages.deleteIconButton) }}
</Button>
</span>
</template>
@ -70,17 +70,21 @@
/>
</div>
<label for="collection-title">
<span class="label__title"> Title </span>
<span class="label__title"> {{ formatMessage(commonMessages.titleLabel) }} </span>
</label>
<input id="collection-title" v-model="name" maxlength="255" type="text" />
<label for="collection-description">
<span class="label__title"> Description </span>
<span class="label__title">
{{ formatMessage(commonMessages.descriptionLabel) }}
</span>
</label>
<div class="textarea-wrapper">
<textarea id="collection-description" v-model="summary" maxlength="255" />
</div>
<label for="visibility">
<span class="label__title"> Visibility </span>
<span class="label__title">
{{ formatMessage(commonMessages.visibilityLabel) }}
</span>
</label>
<DropdownSelect
id="visibility"
@ -90,8 +94,8 @@
:multiple="false"
:display-name="
(s) => {
if (s === 'listed') return 'Public'
return capitalizeString(s)
if (s === 'listed') return formatMessage(commonMessages.publicLabel)
return formatMessage(commonMessages[`${s}Label`])
}
"
:searchable="false"
@ -100,11 +104,11 @@
<div class="push-right input-group">
<Button @click="isEditing = false">
<XIcon />
Cancel
{{ formatMessage(commonMessages.cancelButton) }}
</Button>
<Button color="primary" @click="saveChanges()">
<SaveIcon />
Save
{{ formatMessage(commonMessages.saveButton) }}
</Button>
</div>
</template>
@ -117,7 +121,9 @@
<h1 class="title">{{ collection.name }}</h1>
<div>
<span class="collection-label"><BoxIcon /> Collection</span>
<span class="collection-label">
<BoxIcon /> {{ formatMessage(messages.collectionLabel) }}
</span>
</div>
<div class="collection-info">
@ -131,25 +137,25 @@
<template v-if="collection.status === 'listed'">
<WorldIcon class="primary-stat__icon" aria-hidden="true" />
<div class="primary-stat__text">
<strong> Public </strong>
<strong> {{ formatMessage(commonMessages.publicLabel) }} </strong>
</div>
</template>
<template v-else-if="collection.status === 'unlisted'">
<LinkIcon class="primary-stat__icon" aria-hidden="true" />
<div class="primary-stat__text">
<strong> Unlisted </strong>
<strong> {{ formatMessage(commonMessages.unlistedLabel) }} </strong>
</div>
</template>
<template v-else-if="collection.status === 'private'">
<LockIcon class="primary-stat__icon" aria-hidden="true" />
<div class="primary-stat__text">
<strong> Private </strong>
<strong> {{ formatMessage(commonMessages.privateLabel) }} </strong>
</div>
</template>
<template v-else-if="collection.status === 'rejected'">
<XIcon class="primary-stat__icon" aria-hidden="true" />
<div class="primary-stat__text">
<strong> Rejected </strong>
<strong> {{ formatMessage(commonMessages.rejectedLabel) }} </strong>
</div>
</template>
</div>
@ -158,35 +164,57 @@
<div class="primary-stat">
<LibraryIcon class="primary-stat__icon" aria-hidden="true" />
<div v-if="projects" class="primary-stat__text">
<IntlFormatted
:message-id="messages.projectsCountLabel"
:values="{ count: formatCompactNumber(projects.length || 0) }"
>
<template #stat="{ children }">
<span class="primary-stat__counter">
{{ $formatNumber(projects?.length || 0) }}
<component :is="() => normalizeChildren(children)" />
</span>
project<span v-if="projects?.length !== 1">s</span>
</template>
</IntlFormatted>
</div>
</div>
<div class="metadata-item">
<div
v-tooltip="$dayjs(collection.created).format('MMMM D, YYYY [at] h:mm A')"
v-tooltip="
formatMessage(commonMessages.dateAtTimeTooltip, {
date: new Date(collection.created),
time: new Date(collection.created),
})
"
class="date"
>
<CalendarIcon />
<label>
Created
{{ fromNow(collection.created) }}
{{
formatMessage(messages.createdAtLabel, {
ago: formatRelativeTime(collection.created),
})
}}
</label>
</div>
</div>
<div v-if="collection.id !== 'following'" class="metadata-item">
<div
v-tooltip="$dayjs(collection.updated).format('MMMM D, YYYY [at] h:mm A')"
v-tooltip="
formatMessage(commonMessages.dateAtTimeTooltip, {
date: new Date(collection.updated),
time: new Date(collection.updated),
})
"
class="date"
>
<UpdatedIcon />
<label>
Updated
{{ fromNow(collection.updated) }}
{{
formatMessage(messages.updatedAtLabel, {
ago: formatRelativeTime(collection.updated),
})
}}
</label>
</div>
</div>
@ -195,7 +223,7 @@
<hr class="card-divider" />
<div class="collection-info">
<h2 class="card-header">Curated by</h2>
<h2 class="card-header">{{ formatMessage(messages.curatedByLabel) }}</h2>
<div class="metadata-item">
<nuxt-link
class="team-member columns button-transparent"
@ -205,7 +233,7 @@
<div class="member-info">
<p class="name">{{ creator.username }}</p>
<p class="role">Owner</p>
<p class="role">{{ formatMessage(messages.ownerLabel) }}</p>
</div>
</nuxt-link>
</div>
@ -304,7 +332,7 @@
"
>
<TrashIcon />
Remove project
{{ formatMessage(messages.removeProjectButton) }}
</button>
<button
v-if="collection.id === 'following'"
@ -312,18 +340,22 @@
@click="unfollowProject(project)"
>
<TrashIcon />
Unfollow project
{{ formatMessage(messages.unfollowProjectButton) }}
</button>
</ProjectCard>
</div>
<div v-else class="error">
<UpToDate class="icon" /><br />
<span v-if="auth.user && auth.user.id === creator.id" class="text">
You don't have any projects.<br />
Would you like to
<a class="link" @click.prevent="$router.push('/mods')"> add one</a>?
<span v-if="auth.user && auth.user.id === creator.id" class="preserve-lines text">
<IntlFormatted :message-id="messages.noProjectsAuthLabel">
<template #create-link="{ children }">
<a class="link" @click.prevent="$router.push('/mods')">
<component :is="() => children" />
</a>
</template>
</IntlFormatted>
</span>
<span v-else class="text">This collection has no projects!</span>
<span v-else class="text">{{ formatMessage(messages.noProjectsLabel) }}</span>
</div>
</div>
</div>
@ -332,7 +364,6 @@
<script setup>
import {
capitalizeString,
Avatar,
Button,
CalendarIcon,
@ -364,6 +395,89 @@ import ProjectCard from '~/components/ui/ProjectCard.vue'
const vintl = useVIntl()
const { formatMessage } = vintl
const formatRelativeTime = useRelativeTime()
const formatCompactNumber = useCompactNumber()
const messages = defineMessages({
collectionDescription: {
id: 'collection.description',
defaultMessage: '{description} - View the collection {name} by {username} on Modrinth',
},
collectionLabel: {
id: 'collection.label.collection',
defaultMessage: 'Collection',
},
collectionTitle: {
id: 'collection.title',
defaultMessage: '{name} - Collection',
},
editIconButton: {
id: 'collection.button.edit-icon',
defaultMessage: 'Edit icon',
},
deleteIconButton: {
id: 'collection.button.delete-icon',
defaultMessage: 'Delete icon',
},
createdAtLabel: {
id: 'collection.label.created-at',
defaultMessage: 'Created {ago}',
},
collectionNotFoundError: {
id: 'collection.error.not-found',
defaultMessage: 'Collection not found',
},
curatedByLabel: {
id: 'collection.label.curated-by',
defaultMessage: 'Curated by',
},
deleteModalDescription: {
id: 'collection.delete-modal.description',
defaultMessage: 'This will remove this collection forever. This action cannot be undone.',
},
deleteModalTitle: {
id: 'collection.delete-modal.title',
defaultMessage: 'Are you sure you want to delete this collection?',
},
followingCollectionDescription: {
id: 'collection.description.following',
defaultMessage: "Auto-generated collection of all the projects you're following.",
},
noProjectsLabel: {
id: 'collection.label.no-projects',
defaultMessage: 'This collection has no projects!',
},
noProjectsAuthLabel: {
id: 'collection.label.no-projects-auth',
defaultMessage:
"You don't have any projects.\nWould you like to <create-link>add one</create-link>?",
},
ownerLabel: {
id: 'collection.label.owner',
defaultMessage: 'Owner',
},
projectsCountLabel: {
id: 'collection.label.projects-count',
defaultMessage:
'{count, plural, one {<stat>{count}</stat> project} other {<stat>{count}</stat> projects}}',
},
removeProjectButton: {
id: 'collection.button.remove-project',
defaultMessage: 'Remove project',
},
unfollowProjectButton: {
id: 'collection.button.unfollow-project',
defaultMessage: 'Unfollow project',
},
updatedAtLabel: {
id: 'collection.label.updated-at',
defaultMessage: 'Updated {ago}',
},
uploadIconButton: {
id: 'collection.button.upload-icon',
defaultMessage: 'Upload icon',
},
})
const data = useNuxtApp()
const route = useRoute()
@ -388,8 +502,8 @@ try {
collection = ref({
id: 'following',
icon_url: 'https://cdn.modrinth.com/follow-collection.png',
name: 'Followed projects',
description: "Auto-generated collection of all the projects you're following.",
name: formatMessage(commonMessages.followedProjectsLabel),
description: formatMessage(messages.followingCollectionDescription),
status: 'private',
user: auth.value.user.id,
created: auth.value.user.created,
@ -426,7 +540,7 @@ try {
throw createError({
fatal: true,
statusCode: 404,
message: 'Collection not found',
message: formatMessage(messages.collectionNotFoundError),
})
}
@ -434,16 +548,22 @@ if (!collection.value) {
throw createError({
fatal: true,
statusCode: 404,
message: 'Collection not found',
message: formatMessage(messages.collectionNotFoundError),
})
}
const title = `${collection.value.name} - Collection`
const description = `${collection.value.description} - View the collection ${collection.value.description} by ${creator.value.username} on Modrinth`
const title = computed(() =>
formatMessage(messages.collectionTitle, { name: collection.value.name })
)
useSeoMeta({
title,
description,
description: () =>
formatMessage(messages.collectionDescription, {
name: collection.value.name,
description: collection.value.description,
username: creator.value.username,
}),
ogTitle: title,
ogDescription: collection.value.description,
ogImage: collection.value.icon_url ?? 'https://cdn.modrinth.com/placeholder.png',
@ -526,7 +646,7 @@ async function saveChanges() {
} catch (err) {
addNotification({
group: 'main',
title: 'An error occurred',
title: formatMessage(commonMessages.errorNotificationTitle),
text: err,
type: 'error',
})
@ -546,7 +666,7 @@ async function deleteCollection() {
} catch (err) {
addNotification({
group: 'main',
title: 'An error occurred',
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data.description,
type: 'error',
})

View File

@ -24,7 +24,10 @@
<NavStackItem v-if="true" link="/dashboard/organizations" label="Organizations">
<OrganizationIcon />
</NavStackItem>
<NavStackItem link="/dashboard/collections" label="Collections">
<NavStackItem
link="/dashboard/collections"
:label="formatMessage(commonMessages.collectionsLabel)"
>
<LibraryIcon />
</NavStackItem>
<NavStackItem link="/dashboard/revenue" label="Revenue">
@ -50,6 +53,8 @@ import ReportIcon from '~/assets/images/utils/report.svg'
import NotificationsIcon from '~/assets/images/utils/bell.svg'
import OrganizationIcon from '~/assets/images/utils/organization.svg'
const { formatMessage } = useVIntl()
definePageMeta({
middleware: 'auth',
})

View File

@ -1,10 +1,10 @@
<template>
<div class="universal-card">
<CollectionCreateModal ref="modal_creation" />
<h2>Collections</h2>
<h2>{{ formatMessage(commonMessages.collectionsLabel) }}</h2>
<div class="search-row">
<div class="iconified-input">
<label for="search-input" hidden>Search your collections</label>
<label for="search-input" hidden>{{ formatMessage(messages.searchInputLabel) }}</label>
<SearchIcon />
<input id="search-input" v-model="filterQuery" type="text" />
<Button v-if="filterQuery" class="r-btn" @click="() => (filterQuery = '')">
@ -12,7 +12,7 @@
</Button>
</div>
<Button color="primary" @click="$refs.modal_creation.show()">
<PlusIcon /> Create new
<PlusIcon /> {{ formatMessage(messages.createNewButton) }}
</Button>
</div>
<div class="collections-grid">
@ -23,13 +23,22 @@
>
<Avatar src="https://cdn.modrinth.com/follow-collection.png" class="icon" />
<div class="details">
<span class="title">Followed projects</span>
<span class="title">{{ formatMessage(commonMessages.followedProjectsLabel) }}</span>
<span class="description">
Auto-generated collection of all the projects you're following.
{{ formatMessage(messages.followingCollectionDescription) }}
</span>
<div class="stat-bar">
<div class="stats"><BoxIcon /> {{ user.follows.length }} projects</div>
<div class="stats"><LockIcon /> <span> Private </span></div>
<div class="stats">
<BoxIcon />
{{
formatMessage(messages.projectsCountLabel, {
count: formatCompactNumber(user.follows.length),
})
}}
</div>
<div class="stats">
<LockIcon /> <span> {{ formatMessage(commonMessages.privateLabel) }} </span>
</div>
</div>
</div>
</nuxt-link>
@ -46,23 +55,30 @@
{{ collection.description }}
</span>
<div class="stat-bar">
<div class="stats"><BoxIcon /> {{ collection.projects?.length || 0 }} projects</div>
<div class="stats">
<BoxIcon />
{{
formatMessage(messages.projectsCountLabel, {
count: formatCompactNumber(collection.projects?.length || 0),
})
}}
</div>
<div class="stats">
<template v-if="collection.status === 'listed'">
<WorldIcon />
<span> Public </span>
<span> {{ formatMessage(commonMessages.publicLabel) }} </span>
</template>
<template v-else-if="collection.status === 'unlisted'">
<LinkIcon />
<span> Unlisted </span>
<span> {{ formatMessage(commonMessages.unlistedLabel) }} </span>
</template>
<template v-else-if="collection.status === 'private'">
<LockIcon />
<span> Private </span>
<span> {{ formatMessage(commonMessages.privateLabel) }} </span>
</template>
<template v-else-if="collection.status === 'rejected'">
<XIcon />
<span> Rejected </span>
<span> {{ formatMessage(commonMessages.rejectedLabel) }} </span>
</template>
</div>
</div>
@ -76,12 +92,38 @@ import { Avatar, BoxIcon, SearchIcon, XIcon, Button, PlusIcon, LinkIcon, LockIco
import WorldIcon from '~/assets/images/utils/world.svg'
import CollectionCreateModal from '~/components/ui/CollectionCreateModal.vue'
const { formatMessage } = useVIntl()
const formatCompactNumber = useCompactNumber()
const messages = defineMessages({
createNewButton: {
id: 'dashboard.collections.button.create-new',
defaultMessage: 'Create new',
},
collectionsLongTitle: {
id: 'dashboard.collections.long-title',
defaultMessage: 'Your collections',
},
followingCollectionDescription: {
id: 'collection.description.following',
defaultMessage: "Auto-generated collection of all the projects you're following.",
},
projectsCountLabel: {
id: 'dashboard.collections.label.projects-count',
defaultMessage: '{count, plural, one {{count} project} other {{count} projects}}',
},
searchInputLabel: {
id: 'dashboard.collections.label.search-input',
defaultMessage: 'Search your collections',
},
})
definePageMeta({
middleware: 'auth',
})
useHead({
title: 'Your collections - Modrinth',
title: () => `${formatMessage(messages.collectionsLongTitle)} - Modrinth`,
})
const user = await useUser()

View File

@ -7,6 +7,10 @@ export const commonMessages = defineMessages({
id: 'button.cancel',
defaultMessage: 'Cancel',
},
collectionsLabel: {
id: 'label.collections',
defaultMessage: 'Collections',
},
continueButton: {
id: 'button.continue',
defaultMessage: 'Continue',
@ -15,10 +19,26 @@ export const commonMessages = defineMessages({
id: 'tooltip.date-at-time',
defaultMessage: '{date, date, long} at {time, time, short}',
},
deleteLabel: {
id: 'label.delete',
defaultMessage: 'Delete',
},
descriptionLabel: {
id: 'label.description',
defaultMessage: 'Description',
},
editButton: {
id: 'button.edit',
defaultMessage: 'Edit',
},
errorNotificationTitle: {
id: 'notification.error.title',
defaultMessage: 'An error occurred',
},
followedProjectsLabel: {
id: 'label.followed-projects',
defaultMessage: 'Followed projects',
},
galleryInputView: {
id: 'input.view.gallery',
defaultMessage: 'Gallery view',
@ -31,12 +51,32 @@ export const commonMessages = defineMessages({
id: 'input.view.list',
defaultMessage: 'List view',
},
errorNotificationTitle: {
id: 'notification.error.title',
defaultMessage: 'An error occurred',
privateLabel: {
id: 'collection.label.private',
defaultMessage: 'Private',
},
publicLabel: {
id: 'label.public',
defaultMessage: 'Public',
},
rejectedLabel: {
id: 'label.rejected',
defaultMessage: 'Rejected',
},
saveButton: {
id: 'button.save',
defaultMessage: 'Save',
},
titleLabel: {
id: 'label.title',
defaultMessage: 'Title',
},
unlistedLabel: {
id: 'label.unlisted',
defaultMessage: 'Unlisted',
},
visibilityLabel: {
id: 'label.visibility',
defaultMessage: 'Visibility',
},
})