You've already forked AstralRinth
forked from didirus/AstralRinth
Add translations for profile page (#1340)
Co-authored-by: Sasha Sorokin <10401817+brawaru@users.noreply.github.com>
This commit is contained in:
@@ -33,3 +33,7 @@ body {
|
||||
position: absolute !important;
|
||||
}
|
||||
}
|
||||
|
||||
.preserve-lines {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
34
utils/common-messages.ts
Normal 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',
|
||||
},
|
||||
})
|
||||
58
utils/i18n-project-type.ts
Normal file
58
utils/i18n-project-type.ts
Normal 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
25
utils/vue-children.ts
Normal 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)]
|
||||
}
|
||||
Reference in New Issue
Block a user