Add translations for profile page (#1340)

Co-authored-by: Sasha Sorokin <10401817+brawaru@users.noreply.github.com>
This commit is contained in:
Mysterious_Dev
2023-09-02 15:37:04 +02:00
committed by GitHub
parent 9895976128
commit 75e075ef8e
6 changed files with 379 additions and 32 deletions

View File

@@ -33,3 +33,7 @@ body {
position: absolute !important;
}
}
.preserve-lines {
white-space: pre-line;
}

View File

@@ -1,4 +1,13 @@
{
"button.cancel": {
"message": "Cancel"
},
"button.edit": {
"message": "Edit"
},
"button.save": {
"message": "Save"
},
"frog": {
"message": "You've been frogged! 🐸"
},
@@ -14,6 +23,102 @@
"frog.title": {
"message": "Frog"
},
"input.view.gallery": {
"message": "Gallery view"
},
"input.view.grid": {
"message": "Grid view"
},
"input.view.list": {
"message": "List view"
},
"notification.error.title": {
"message": "An error occurred"
},
"profile.button.manage-projects": {
"message": "Manage projects"
},
"profile.button.report": {
"message": "Report"
},
"profile.error.not-found": {
"message": "User not found"
},
"profile.input.upload-avatar": {
"message": "Upload avatar"
},
"profile.joined-at": {
"message": "Joined {ago}"
},
"profile.joined-at.tooltip": {
"message": "{date, date, long} at {time, time, short}"
},
"profile.label.edit-bio": {
"message": "Bio"
},
"profile.label.edit-username": {
"message": "Username"
},
"profile.label.no-projects": {
"message": "This user has no projects!"
},
"profile.label.no-projects-auth": {
"message": "You don't have any projects.\nWould you like to <create-link>create one</create-link>?"
},
"profile.meta.description": {
"message": "Download {username}'s projects on Modrinth"
},
"profile.meta.description-with-bio": {
"message": "{bio} - Download {username}'s projects on Modrinth"
},
"profile.stats.downloads": {
"message": "{count, plural, one {<stat>{count}</stat> download} other {<stat>{count}</stat> downloads}}"
},
"profile.stats.projects-followers": {
"message": "{count, plural, one {<stat>{count}</stat> follower} other {<stat>{count}</stat> followers}} of projects"
},
"profile.user-id": {
"message": "User ID: {id}"
},
"project-type.all": {
"message": "All"
},
"project-type.datapack.plural": {
"message": "Data Packs"
},
"project-type.datapack.singular": {
"message": "Data Pack"
},
"project-type.mod.plural": {
"message": "Mods"
},
"project-type.mod.singular": {
"message": "Mod"
},
"project-type.modpack.plural": {
"message": "Modpacks"
},
"project-type.modpack.singular": {
"message": "Modpack"
},
"project-type.plugin.plural": {
"message": "Plugins"
},
"project-type.plugin.singular": {
"message": "Plugin"
},
"project-type.resourcepack.plural": {
"message": "Resource Packs"
},
"project-type.resourcepack.singular": {
"message": "Resource Pack"
},
"project-type.shader.plural": {
"message": "Shaders"
},
"project-type.shader.singular": {
"message": "Shader"
},
"settings.language.categories.auto": {
"message": "Automatic"
},

View File

@@ -38,9 +38,9 @@
v-if="isEditing"
:max-size="262144"
:show-icon="true"
:prompt="formatMessage(messages.profileUploadAvatarInput)"
accept="image/png,image/jpeg,image/gif,image/webp"
class="choose-image iconified-button"
prompt="Upload avatar"
@change="showPreviewImage"
>
<UploadIcon />
@@ -51,7 +51,7 @@
@click="isEditing = true"
>
<EditIcon />
Edit
{{ formatMessage(commonMessages.editButton) }}
</button>
<button
v-else-if="auth.user"
@@ -59,18 +59,26 @@
@click="$refs.modal_report.show()"
>
<ReportIcon aria-hidden="true" />
Report
{{ formatMessage(messages.profileReportButton) }}
</button>
<nuxt-link v-else class="iconified-button" to="/auth/sign-in">
<ReportIcon aria-hidden="true" />
Report
{{ formatMessage(messages.profileReportButton) }}
</nuxt-link>
</div>
<template v-if="isEditing">
<div class="inputs universal-labels">
<label for="user-username"><span class="label__title">Username</span></label>
<label for="user-username">
<span class="label__title">
{{ formatMessage(messages.profileEditUsernameLabel) }}
</span>
</label>
<input id="user-username" v-model="user.username" maxlength="39" type="text" />
<label for="user-bio"><span class="label__title">Bio</span></label>
<label for="user-bio">
<span class="label__title">
{{ formatMessage(messages.profileEditBioLabel) }}
</span>
</label>
<div class="textarea-wrapper">
<textarea id="user-bio" v-model="user.bio" maxlength="160" />
</div>
@@ -87,10 +95,10 @@
}
"
>
<CrossIcon /> Cancel
<CrossIcon /> {{ formatMessage(commonMessages.cancelButton) }}
</button>
<button class="iconified-button brand-button" @click="saveChanges">
<SaveIcon /> Save
<SaveIcon /> {{ formatMessage(commonMessages.saveButton) }}
</button>
</div>
</template>
@@ -104,30 +112,59 @@
<div class="primary-stat">
<DownloadIcon class="primary-stat__icon" aria-hidden="true" />
<div class="primary-stat__text">
<span class="primary-stat__counter">{{ sumDownloads }}</span>
downloads
<IntlFormatted
:message-id="messages.profileDownloadsStats"
:values="{ count: formatCompactNumber(sumDownloads) }"
>
<template #stat="{ children }">
<span class="primary-stat__counter">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</div>
</div>
<div class="primary-stat">
<HeartIcon class="primary-stat__icon" aria-hidden="true" />
<div class="primary-stat__text">
<span class="primary-stat__counter">{{ sumFollows }}</span>
followers of projects
<IntlFormatted
:message-id="messages.profileProjectsFollowersStats"
:values="{ count: formatCompactNumber(sumFollows) }"
>
<template #stat="{ children }">
<span class="primary-stat__counter">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</div>
</div>
<div class="stats-block__item secondary-stat">
<SunriseIcon class="secondary-stat__icon" aria-hidden="true" />
<span
v-tooltip="$dayjs(user.created).format('MMMM D, YYYY [at] h:mm A')"
v-tooltip="
formatMessage(messages.profileJoinedAtTooltip, {
date: new Date(user.created),
time: new Date(user.created),
})
"
class="secondary-stat__text date"
>
Joined {{ fromNow(user.created) }}
{{
formatMessage(messages.profileJoinedAt, { ago: formatRelativeTime(user.created) })
}}
</span>
</div>
<hr class="card-divider" />
<div class="stats-block__item secondary-stat">
<UserIcon class="secondary-stat__icon" aria-hidden="true" />
<span class="secondary-stat__text"> User ID: <CopyCode :text="user.id" /> </span>
<span class="secondary-stat__text">
<IntlFormatted :message-id="messages.profileUserId">
<template #~id>
<CopyCode :text="user.id" />
</template>
</IntlFormatted>
</span>
</div>
</template>
</div>
@@ -138,12 +175,12 @@
<NavRow
:links="[
{
label: 'all',
label: formatMessage(commonMessages.allProjectType),
href: `/user/${user.username}`,
},
...projectTypes.map((x) => {
return {
label: $formatProjectType(x) + 's',
label: formatMessage(getProjectTypeMessage(x, true)),
href: `/user/${user.username}/${x}s`,
}
}),
@@ -156,11 +193,15 @@
to="/dashboard/projects"
>
<SettingsIcon />
Manage projects
{{ formatMessage(messages.profileManageProjectsButton) }}
</NuxtLink>
<button
v-tooltip="$capitalizeString(cosmetics.searchDisplayMode.user) + ' view'"
:aria-label="$capitalizeString(cosmetics.searchDisplayMode.user) + ' view'"
v-tooltip="
formatMessage(commonMessages[`${cosmetics.searchDisplayMode.user}InputView`])
"
:aria-label="
formatMessage(commonMessages[`${cosmetics.searchDisplayMode.user}InputView`])
"
class="square-button"
@click="cycleSearchDisplayMode()"
>
@@ -216,12 +257,16 @@
</div>
<div v-else class="error">
<UpToDate class="icon" /><br />
<span v-if="auth.user && auth.user.id === user.id" class="text">
You don't have any projects.<br />
Would you like to
<a class="link" @click.prevent="$refs.modal_creation.show()"> create one</a>?
<span v-if="auth.user && auth.user.id === user.id" class="preserve-lines text">
<IntlFormatted :message-id="messages.profileNoProjectsAuthLabel">
<template #create-link="{ children }">
<a class="link" @click.prevent="$refs.modal_creation.show()">
<component :is="() => children" />
</a>
</template>
</IntlFormatted>
</span>
<span v-else class="text">This user has no projects!</span>
<span v-else class="text">{{ formatMessage(messages.profileNoProjectsLabel) }}</span>
</div>
</div>
</div>
@@ -259,6 +304,79 @@ const auth = await useAuth()
const cosmetics = useCosmetics()
const tags = useTags()
const vintl = useVIntl()
const { formatMessage } = vintl
const formatCompactNumber = useCompactNumber()
const formatRelativeTime = useRelativeTime()
const messages = defineMessages({
profileDownloadsStats: {
id: 'profile.stats.downloads',
defaultMessage:
'{count, plural, one {<stat>{count}</stat> download} other {<stat>{count}</stat> downloads}}',
},
profileProjectsFollowersStats: {
id: 'profile.stats.projects-followers',
defaultMessage:
'{count, plural, one {<stat>{count}</stat> follower} other {<stat>{count}</stat> followers}} of projects',
},
profileJoinedAt: {
id: 'profile.joined-at',
defaultMessage: 'Joined {ago}',
},
profileJoinedAtTooltip: {
id: 'profile.joined-at.tooltip',
defaultMessage: '{date, date, long} at {time, time, short}',
},
profileUserId: {
id: 'profile.user-id',
defaultMessage: 'User ID: {id}',
},
profileManageProjectsButton: {
id: 'profile.button.manage-projects',
defaultMessage: 'Manage projects',
},
profileMetaDescription: {
id: 'profile.meta.description',
defaultMessage: "Download {username}'s projects on Modrinth",
},
profileMetaDescriptionWithBio: {
id: 'profile.meta.description-with-bio',
defaultMessage: "{bio} - Download {username}'s projects on Modrinth",
},
profileReportButton: {
id: 'profile.button.report',
defaultMessage: 'Report',
},
profileUploadAvatarInput: {
id: 'profile.input.upload-avatar',
defaultMessage: 'Upload avatar',
},
profileEditUsernameLabel: {
id: 'profile.label.edit-username',
defaultMessage: 'Username',
},
profileEditBioLabel: {
id: 'profile.label.edit-bio',
defaultMessage: 'Bio',
},
profileNoProjectsLabel: {
id: 'profile.label.no-projects',
defaultMessage: 'This user has no projects!',
},
profileNoProjectsAuthLabel: {
id: 'profile.label.no-projects-auth',
defaultMessage:
"You don't have any projects.\n Would you like to <create-link>create one</create-link>?",
},
userNotFoundError: {
id: 'profile.error.not-found',
defaultMessage: 'User not found',
},
})
let user, projects
try {
;[{ data: user }, { data: projects }] = await Promise.all([
@@ -286,7 +404,7 @@ try {
throw createError({
fatal: true,
statusCode: 404,
message: 'User not found',
message: formatMessage(messages.userNotFoundError),
})
}
@@ -294,7 +412,7 @@ if (!user.value) {
throw createError({
fatal: true,
statusCode: 404,
message: 'User not found',
message: formatMessage(messages.userNotFoundError),
})
}
@@ -304,8 +422,11 @@ if (user.value.username !== route.params.id) {
const metaDescription = ref(
user.value.bio
? `${user.value.bio} - Download ${user.value.username}'s projects on Modrinth`
: `Download ${user.value.username}'s projects on Modrinth`
? `${formatMessage(messages.profileMetaDescriptionWithBio, {
bio: user.value.bio,
username: user.value.username,
})}`
: `${formatMessage(messages.profileMetaDescription, { username: user.value.username })}`
)
const projectTypes = computed(() => {
@@ -324,7 +445,7 @@ const sumDownloads = computed(() => {
sum += project.downloads
}
return data.$formatNumber(sum)
return sum
})
const sumFollows = computed(() => {
let sum = 0
@@ -333,7 +454,7 @@ const sumFollows = computed(() => {
sum += project.followers
}
return data.$formatNumber(sum)
return sum
})
const isEditing = ref(false)
@@ -383,7 +504,7 @@ async function saveChanges() {
console.error(err)
data.$notify({
group: 'main',
title: 'An error occurred',
title: commonMessages.errorNotificationTitle,
text: err.data.description,
type: 'error',
})

34
utils/common-messages.ts Normal file
View File

@@ -0,0 +1,34 @@
export const commonMessages = defineMessages({
allProjectType: {
id: 'project-type.all',
defaultMessage: 'All',
},
cancelButton: {
id: 'button.cancel',
defaultMessage: 'Cancel',
},
editButton: {
id: 'button.edit',
defaultMessage: 'Edit',
},
galleryInputView: {
id: 'input.view.gallery',
defaultMessage: 'Gallery view',
},
gridInputView: {
id: 'input.view.grid',
defaultMessage: 'Grid view',
},
listInputView: {
id: 'input.view.list',
defaultMessage: 'List view',
},
errorNotificationTitle: {
id: 'notification.error.title',
defaultMessage: 'An error occurred',
},
saveButton: {
id: 'button.save',
defaultMessage: 'Save',
},
})

View File

@@ -0,0 +1,58 @@
const projectTypeMessages = defineMessages({
datapack: {
id: 'project-type.datapack.singular',
defaultMessage: 'Data Pack',
},
datapacks: {
id: 'project-type.datapack.plural',
defaultMessage: 'Data Packs',
},
mod: {
id: 'project-type.mod.singular',
defaultMessage: 'Mod',
},
mods: {
id: 'project-type.mod.plural',
defaultMessage: 'Mods',
},
modpack: {
id: 'project-type.modpack.singular',
defaultMessage: 'Modpack',
},
modpacks: {
id: 'project-type.modpack.plural',
defaultMessage: 'Modpacks',
},
plugin: {
id: 'project-type.plugin.singular',
defaultMessage: 'Plugin',
},
plugins: {
id: 'project-type.plugin.plural',
defaultMessage: 'Plugins',
},
resourcepack: {
id: 'project-type.resourcepack.singular',
defaultMessage: 'Resource Pack',
},
resourcepacks: {
id: 'project-type.resourcepack.plural',
defaultMessage: 'Resource Packs',
},
shader: {
id: 'project-type.shader.singular',
defaultMessage: 'Shader',
},
shaders: {
id: 'project-type.shader.plural',
defaultMessage: 'Shaders',
},
})
type ExtractSingulars<K extends string> = K extends `${infer T}s` ? T : never
type ProjectType = ExtractSingulars<keyof typeof projectTypeMessages>
export function getProjectTypeMessage(type: ProjectType, plural = false) {
return projectTypeMessages[`${type}${plural ? 's' : ''}`]
}

25
utils/vue-children.ts Normal file
View File

@@ -0,0 +1,25 @@
import { createTextVNode, isVNode, toDisplayString, type VNode } from 'vue'
/**
* Checks whether a specific child is a VNode. If not, converts it to a display
* string and then creates text VNode for the result.
*
* @param child Child to normalize.
* @returns Either the original VNode or a text VNode containing child converted
* to a display string.
*/
function normalizeChild(child: any): VNode {
return isVNode(child) ? child : createTextVNode(toDisplayString(child))
}
/**
* Takes in an array of VNodes and other children. It then converts each child
* that is not already a VNode to a display string, and creates a text VNode for
* that string.
*
* @param children Children to normalize.
* @returns Children with all of non-VNodes converted to display strings.
*/
export function normalizeChildren(children: any | any[]): VNode[] {
return Array.isArray(children) ? children.map(normalizeChild) : [normalizeChild(children)]
}