diff --git a/apps/frontend/src/pages/[type]/[project]/gallery.vue b/apps/frontend/src/pages/[type]/[project]/gallery.vue
index 1381c150cb..03d026a906 100644
--- a/apps/frontend/src/pages/[type]/[project]/gallery.vue
+++ b/apps/frontend/src/pages/[type]/[project]/gallery.vue
@@ -92,7 +92,7 @@
v-if="editIndex === -1"
type="colored"
color="brand"
- :disabled="shouldPreventActions || galleryFieldsInvalid"
+ :disabled="shouldPreventActions || !canSaveGalleryFields"
@click="createGalleryItem"
>
@@ -102,7 +102,7 @@
v-else
type="colored"
color="brand"
- :disabled="shouldPreventActions || galleryFieldsInvalid"
+ :disabled="shouldPreventActions || !canSaveGalleryFields"
@click="editGalleryItem"
>
@@ -312,6 +312,7 @@ import {
Textarea,
useFormatDateTime,
} from '@modrinth/ui'
+import { isAdmin } from '@modrinth/utils'
import { useEventListener } from '@vueuse/core'
import AiImageWarningModal from '~/components/ui/AiImageWarningModal.vue'
@@ -390,6 +391,8 @@ const galleryFieldsInvalid = computed(
galleryTitleValidation.value.some((validation) => validation.severity === 'error') ||
galleryDescriptionValidation.value.some((validation) => validation.severity === 'error'),
)
+const isAdminUser = computed(() => isAdmin(currentMember.value?.user))
+const canSaveGalleryFields = computed(() => isAdminUser.value || !galleryFieldsInvalid.value)
// Constant for accepted file types
const MC_SERVER_BANNER_NAME = '__mc_server_banner__'
@@ -490,7 +493,7 @@ function showPreviewImage() {
// CRUD operations
async function createGalleryItem() {
- if (galleryFieldsInvalid.value) return
+ if (!canSaveGalleryFields.value) return
shouldPreventActions.value = true
startLoading()
@@ -511,7 +514,7 @@ async function createGalleryItem() {
}
async function editGalleryItem() {
- if (galleryFieldsInvalid.value) return
+ if (!canSaveGalleryFields.value) return
shouldPreventActions.value = true
startLoading()
diff --git a/apps/frontend/src/pages/[type]/[project]/settings/gallery.vue b/apps/frontend/src/pages/[type]/[project]/settings/gallery.vue
index 3903fedd7b..75f411f35f 100644
--- a/apps/frontend/src/pages/[type]/[project]/settings/gallery.vue
+++ b/apps/frontend/src/pages/[type]/[project]/settings/gallery.vue
@@ -92,7 +92,7 @@
v-if="editIndex === -1"
type="colored"
color="brand"
- :disabled="shouldPreventActions || galleryFieldsInvalid"
+ :disabled="shouldPreventActions || !canSaveGalleryFields"
@click="createGalleryItem"
>
@@ -102,7 +102,7 @@
v-else
type="colored"
color="brand"
- :disabled="shouldPreventActions || galleryFieldsInvalid"
+ :disabled="shouldPreventActions || !canSaveGalleryFields"
@click="editGalleryItem"
>
@@ -303,6 +303,7 @@ import {
Textarea,
useFormatDateTime,
} from '@modrinth/ui'
+import { isAdmin } from '@modrinth/utils'
import AiImageWarningModal from '~/components/ui/AiImageWarningModal.vue'
import ValidationMessage from '~/components/ValidationMessage.vue'
@@ -350,6 +351,8 @@ const galleryFieldsInvalid = computed(
galleryTitleValidation.value.some((validation) => validation.severity === 'error') ||
galleryDescriptionValidation.value.some((validation) => validation.severity === 'error'),
)
+const isAdminUser = computed(() => isAdmin(currentMember.value?.user))
+const canSaveGalleryFields = computed(() => isAdminUser.value || !galleryFieldsInvalid.value)
const MC_SERVER_BANNER_NAME = '__mc_server_banner__'
const acceptFileTypes = 'image/png,image/jpeg,image/gif,image/webp,.png,.jpeg,.gif,.webp'
@@ -430,7 +433,7 @@ const showPreviewImage = () => {
}
const createGalleryItem = async () => {
- if (galleryFieldsInvalid.value) return
+ if (!canSaveGalleryFields.value) return
shouldPreventActions.value = true
const success = await createGalleryItemMutation(
@@ -449,7 +452,7 @@ const createGalleryItem = async () => {
}
const editGalleryItem = async () => {
- if (galleryFieldsInvalid.value) return
+ if (!canSaveGalleryFields.value) return
shouldPreventActions.value = true
const success = await editGalleryItemMutation(
diff --git a/packages/moderation/src/data/nags/project-validation.ts b/packages/moderation/src/data/nags/project-validation.ts
index 61cbd32e11..573c65165d 100644
--- a/packages/moderation/src/data/nags/project-validation.ts
+++ b/packages/moderation/src/data/nags/project-validation.ts
@@ -33,14 +33,6 @@ const descriptionErrorCodes: readonly ProjectTextValidationCode[] = [
'text-non-standard',
]
-const messages = {
- galleryFailure: defineMessage({
- id: 'nags.invalid-gallery-text.description',
- defaultMessage:
- 'Gallery image {number} {field, select, gallery-name {name} gallery-description {description} other {text}}: {error}',
- }),
-}
-
function getFirstFailure(
context: NagContext,
fields: readonly ProjectValidationField[],
@@ -80,18 +72,6 @@ function getCodedFailureDescription(context: NagContext, code: ProjectTextValida
return formatMessage(failure.message, failure.values)
}
-function getGalleryFailureDescription(context: NagContext): string {
- const failure = getFirstFailure(context, ['gallery-name', 'gallery-description'], 'error')
- if (!failure) return ''
-
- const { formatMessage } = useVIntl()
- return formatMessage(messages.galleryFailure, {
- number: (failure.galleryIndex ?? 0) + 1,
- field: failure.field,
- error: formatMessage(failure.message, failure.values),
- })
-}
-
export const projectValidationNags: Nag[] = [
{
id: 'invalid-project-name',
@@ -341,7 +321,8 @@ export const projectValidationNags: Nag[] = [
id: 'nags.invalid-gallery-text.title',
defaultMessage: 'Fix gallery text',
}),
- description: getGalleryFailureDescription,
+ description: (context) =>
+ getFailureDescription(context, ['gallery-name', 'gallery-description'], 'error'),
status: 'required',
shouldShow: (context) =>
getFirstFailure(context, ['gallery-name', 'gallery-description'], 'error') !== undefined,
diff --git a/packages/moderation/src/locales/en-US/index.json b/packages/moderation/src/locales/en-US/index.json
index c4b84aa774..9cfb1350c1 100644
--- a/packages/moderation/src/locales/en-US/index.json
+++ b/packages/moderation/src/locales/en-US/index.json
@@ -1,380 +1,380 @@
{
- "nags.add-description.description": {
- "defaultMessage": "A description that clearly describes the project's purpose and function is required."
- },
- "nags.add-description.title": {
- "defaultMessage": "Add a description"
- },
- "nags.add-icon.description": {
- "defaultMessage": "Adding a unique, relevant, and engaging icon makes your project identifiable and helps it stand out."
- },
- "nags.add-icon.title": {
- "defaultMessage": "Add an icon"
- },
- "nags.add-java-address.description": {
- "defaultMessage": "Add the IP address and port Java Edition players can use to join your server."
- },
- "nags.add-java-address.title": {
- "defaultMessage": "Add a Java address"
- },
- "nags.add-license-details.description": {
- "defaultMessage": "Add a valid URL and name or SPDX identifier for your custom license."
- },
- "nags.add-license-details.title": {
- "defaultMessage": "Add license details"
- },
- "nags.add-links-server.description": {
- "defaultMessage": "Add any relevant links targeted outside of Modrinth, such as a website, store, or a Discord invite."
- },
- "nags.add-links-server.title": {
- "defaultMessage": "Add external links"
- },
- "nags.add-links.description": {
- "defaultMessage": "Add any relevant links targeted outside of Modrinth, such as source code, an issue tracker, or a Discord invite."
- },
- "nags.add-links.title": {
- "defaultMessage": "Add external links"
- },
- "nags.all-languages.description": {
- "defaultMessage": "You've selected all available language options. Please list only the languages your server actively supports."
- },
- "nags.all-languages.title": {
- "defaultMessage": "Select accurate languages"
- },
- "nags.all-tags-selected.description": {
- "defaultMessage": "You've selected all {totalAvailableTags, plural, one {# available tag} other {# available tags}}. This defeats the purpose of tags, which are meant to help users find relevant projects. Please select only the tags that are relevant to your project."
- },
- "nags.all-tags-selected.title": {
- "defaultMessage": "Select accurate tags"
- },
- "nags.check-disclosures.description": {
- "defaultMessage": "Make sure users are aware of any important details by filling in content disclosures that apply to your {type}."
- },
- "nags.check-disclosures.title": {
- "defaultMessage": "Check content disclosures"
- },
- "nags.description-too-short.description": {
- "defaultMessage": "Your description is {length, plural, one {# readable character} other {# readable characters}}. At least {minChars, plural, one {# character} other {# characters}} is recommended to create a clear and informative description."
- },
- "nags.description-too-short.title": {
- "defaultMessage": "Expand the description"
- },
- "nags.edit-description.title": {
- "defaultMessage": "Edit description"
- },
- "nags.edit-gallery.title": {
- "defaultMessage": "Edit gallery"
- },
- "nags.edit-license.title": {
- "defaultMessage": "Edit license"
- },
- "nags.edit-summary.title": {
- "defaultMessage": "Edit summary"
- },
- "nags.edit-tags.title": {
- "defaultMessage": "Edit tags"
- },
- "nags.edit-title.title": {
- "defaultMessage": "Edit title"
- },
- "nags.feature-gallery-image.description": {
- "defaultMessage": "The featured gallery image is often how your project makes its first impression."
- },
- "nags.feature-gallery-image.title": {
- "defaultMessage": "Feature a gallery image"
- },
- "nags.gallery.title": {
- "defaultMessage": "Visit gallery page"
- },
- "nags.gpl-license-source-required.description": {
- "defaultMessage": "Your {type} uses a license which requires source code to be available. Please provide a source code link or sources file for each additional version, or consider using a different license."
- },
- "nags.gpl-license-source-required.title": {
- "defaultMessage": "Provide source code"
- },
- "nags.identical-links.description": {
- "defaultMessage": "Some of your external links appear to be identical. Each link should be entered only once and with the appropriate link type."
- },
- "nags.identical-links.title": {
- "defaultMessage": "Clean up identical links"
- },
- "nags.image-heavy-description.description": {
- "defaultMessage": "Your Description should contain sufficient plain text or image alt-text, keeping it accessible to those using screen readers or with slow internet connections."
- },
- "nags.image-heavy-description.title": {
- "defaultMessage": "Ensure accessibility"
- },
- "nags.invalid-gallery-text.description": {
- "defaultMessage": "Gallery image {number} {field, select, gallery-name {name} gallery-description {description} other {text}}: {error}"
- },
- "nags.invalid-gallery-text.title": {
- "defaultMessage": "Fix gallery text"
- },
- "nags.invalid-license-url.description.default": {
- "defaultMessage": "License URL is invalid."
- },
- "nags.invalid-license-url.description.domain": {
- "defaultMessage": "Your license URL points to {domain}, which is not appropriate for license information. License URLs should link directly to your license file, not social media, gaming platforms, etc."
- },
- "nags.invalid-license-url.description.malformed": {
- "defaultMessage": "Your license URL appears to be malformed. Please provide a valid URL to your license text."
- },
- "nags.invalid-license-url.title": {
- "defaultMessage": "Add a valid license link"
- },
- "nags.invalid-project-description.title": {
- "defaultMessage": "Fix the project description"
- },
- "nags.invalid-project-name.title": {
- "defaultMessage": "Fix the project name"
- },
- "nags.invalid-project-summary.title": {
- "defaultMessage": "Fix the project summary"
- },
- "nags.link-shortener-usage.description": {
- "defaultMessage": "Use of link shorteners or other methods to obscure where a link may lead in your external links or license link is prohibited, please only use appropriate full length links."
- },
- "nags.link-shortener-usage.title": {
- "defaultMessage": "Don't use link shorteners"
- },
- "nags.link.discord.channel": {
- "defaultMessage": "This is a link to a Discord channel, not a server invite."
- },
- "nags.link.discord.invite.expires": {
- "defaultMessage": "This Discord invite is set to expire"
- },
- "nags.link.discord.invite.invalid": {
- "defaultMessage": "This Discord invite is invalid or has expired."
- },
- "nags.link.discord.invite.not-guild": {
- "defaultMessage": "This Discord invite does not lead to a server."
- },
- "nags.link.discord.user": {
- "defaultMessage": "This is a link to a Discord user, not a server invite."
- },
- "nags.link.expected-type": {
- "defaultMessage": "This isn't a valid {label} link."
- },
- "nags.link.git.archived": {
- "defaultMessage": "This repository is archived, which disables issues."
- },
- "nags.link.git.empty": {
- "defaultMessage": "This repository appears to be empty."
- },
- "nags.link.git.issues-disabled": {
- "defaultMessage": "Issues are disabled on this repository."
- },
- "nags.link.git.not-found": {
- "defaultMessage": "This repository could not be found (it may be private or deleted)."
- },
- "nags.link.git.wiki-disabled": {
- "defaultMessage": "The wiki is disabled on this repository."
- },
- "nags.link.invalid-url": {
- "defaultMessage": "There's an invalid URL in the description."
- },
- "nags.link.license.url-mismatch": {
- "defaultMessage": "This link points to the {detected} license, but your project is set to {selected}."
- },
- "nags.link.license.url-redundant": {
- "defaultMessage": "You don't need to link to a generic license page for a supported license — consider linking to your repository's own license file instead, or leaving this blank."
- },
- "nags.link.never-valid": {
- "defaultMessage": "{label} links aren't allowed here."
- },
- "nags.link.unverifiable-redirect": {
- "defaultMessage": "This doesn't look like a {platform} link."
- },
- "nags.link.wrong-field": {
- "defaultMessage": "{label} links aren't valid for this field."
- },
- "nags.link.youtube.unrecognized": {
- "defaultMessage": "This doesn't look like a YouTube donation link."
- },
- "nags.long-headers.description": {
- "defaultMessage": "{count, plural, one {# header} other {# headers}} in your description {count, plural, one {is} other {are}} too long. Headers should be concise and act as section titles, not full sentences."
- },
- "nags.long-headers.title": {
- "defaultMessage": "Shorten headers"
- },
- "nags.minecraft-title-clause.description": {
- "defaultMessage": "Projects must not use Minecraft's branding or include \"Minecraft\" as a significant part of the name."
- },
- "nags.minecraft-title-clause.title": {
- "defaultMessage": "Avoid brand infringement"
- },
- "nags.missing-alt-text.description": {
- "defaultMessage": "Some of your images are missing alt text, which is important for accessibility, especially for visually impaired users."
- },
- "nags.missing-alt-text.title": {
- "defaultMessage": "Add image alt text"
- },
- "nags.misused-discord-link-description": {
- "defaultMessage": "Discord invites can not be used for other link types. Please put your Discord link in the Discord Invite link field only."
- },
- "nags.misused-discord-link.title": {
- "defaultMessage": "Move Discord invite"
- },
- "nags.moderation.title": {
- "defaultMessage": "Visit moderation thread"
- },
- "nags.moderator-feedback.description": {
- "defaultMessage": "Review and address all concerns from the moderation team before resubmitting."
- },
- "nags.moderator-feedback.title": {
- "defaultMessage": "Review feedback"
- },
- "nags.multiple-resolution-tags.description": {
- "defaultMessage": "You've selected {count, plural, one {# resolution tag} other {# resolution tags}} ({tags}). Resource packs should typically only have one resolution tag that matches their primary resolution."
- },
- "nags.multiple-resolution-tags.title": {
- "defaultMessage": "Select correct resolution"
- },
- "nags.project-name-metadata.title": {
- "defaultMessage": "Remove technical details from the name"
- },
- "nags.project-summary-content.title": {
- "defaultMessage": "Review the project summary"
- },
- "nags.review-permissions.description": {
- "defaultMessage": "Make sure you have provided proof of your permission to distribute any external content in your Modpack."
- },
- "nags.review-permissions.title": {
- "defaultMessage": "Review external permissions"
- },
- "nags.select-compatibility.description": {
- "defaultMessage": "Select what versions your server supports, choose a Modpack, or upload your own."
- },
- "nags.select-compatibility.title": {
- "defaultMessage": "Select compatibility"
- },
- "nags.select-country.description": {
- "defaultMessage": "Let players know what region your server is located in."
- },
- "nags.select-country.title": {
- "defaultMessage": "Select a region"
- },
- "nags.select-language.description": {
- "defaultMessage": "List the language or languages supported by your server."
- },
- "nags.select-language.title": {
- "defaultMessage": "Select a language"
- },
- "nags.select-license.description": {
- "defaultMessage": "Select the license your {type} is distributed under."
- },
- "nags.select-license.title": {
- "defaultMessage": "Select a license"
- },
- "nags.select-tags.description": {
- "defaultMessage": "Select the tags that correctly apply to your project to help the right users find it."
- },
- "nags.select-tags.title": {
- "defaultMessage": "Select tags"
- },
- "nags.server.title": {
- "defaultMessage": "Visit server settings"
- },
- "nags.settings.description.title": {
- "defaultMessage": "Visit description settings"
- },
- "nags.settings.disclosures.title": {
- "defaultMessage": "Visit disclosure settings"
- },
- "nags.settings.license.title": {
- "defaultMessage": "Visit license settings"
- },
- "nags.settings.links.title": {
- "defaultMessage": "Visit links settings"
- },
- "nags.settings.permissions.title": {
- "defaultMessage": "Visit permissions dashboard"
- },
- "nags.settings.tags.title": {
- "defaultMessage": "Visit tag settings"
- },
- "nags.settings.title": {
- "defaultMessage": "Visit general settings"
- },
- "nags.settings.versions.title": {
- "defaultMessage": "Visit versions settings"
- },
- "nags.summary-special-formatting.description": {
- "defaultMessage": "Your summary should not contain formatting, line breaks, or special characters, since the summary will only display plain text."
- },
- "nags.summary-special-formatting.title": {
- "defaultMessage": "Clean up the summary"
- },
- "nags.summary-too-short.title": {
- "defaultMessage": "Expand the summary"
- },
- "nags.too-many-languages.description": {
- "defaultMessage": "You've selected {languageCount, plural, one {# language} other {# languages}}. Please list only the languages your server actively supports."
- },
- "nags.too-many-languages.title": {
- "defaultMessage": "Select accurate languages"
- },
- "nags.too-many-tags-server.description": {
- "defaultMessage": "You've selected {tagCount, plural, one {# tag} other {# tags}}. Please reduce to {maxTagCount} or fewer to make sure your server appears in relevant search results."
- },
- "nags.too-many-tags-server.title": {
- "defaultMessage": "Select accurate tags"
- },
- "nags.too-many-tags.description": {
- "defaultMessage": "You've selected {tagCount, plural, one {# tag} other {# tags}}. Consider reducing to {maxTagCount} or fewer to make sure your project appears in relevant search results."
- },
- "nags.too-many-tags.title": {
- "defaultMessage": "Select accurate tags"
- },
- "nags.upload-gallery-image.description": {
- "defaultMessage": "At least one gallery image is required to showcase the content of your {type}."
- },
- "nags.upload-gallery-image.description-resourcepack": {
- "defaultMessage": "At least one gallery image is required to showcase the content of your resource pack, except for audio or localization packs. If this describes your pack, please select the appropriate tag."
- },
- "nags.upload-gallery-image.description-shader": {
- "defaultMessage": "At least three gallery images are required to showcase the content of your shader in a variety of situations and conditions."
- },
- "nags.upload-gallery-image.title": {
- "defaultMessage": "Upload a gallery image"
- },
- "nags.upload-version.description": {
- "defaultMessage": "At least one version is required for a project to be submitted for review."
- },
- "nags.upload-version.title": {
- "defaultMessage": "Upload a version"
- },
- "nags.verify-external-links.description": {
- "defaultMessage": "Some of your external links may be using domains that are inappropriate for that type of link."
- },
- "nags.verify-external-links.title": {
- "defaultMessage": "Verify external links"
- },
- "nags.visit-links-settings.title": {
- "defaultMessage": "Visit links settings"
- },
- "project.text-validation.non-standard-text": {
- "defaultMessage": "Non-standard text characters are not allowed."
- },
- "project.text-validation.profanity": {
- "defaultMessage": "The detected profanity “{value}” is not allowed."
- },
- "project.text-validation.slur": {
- "defaultMessage": "The detected slur “{value}” is not allowed."
- },
- "project.text-validation.summary-link": {
- "defaultMessage": "Links should not be included in project summaries."
- },
- "project.text-validation.summary-matches-title": {
- "defaultMessage": "A project summary should not be the same as its title."
- },
- "project.text-validation.summary-too-short": {
- "defaultMessage": "Your summary is {length, plural, one {# character} other {# characters}}. At least {minChars, plural, one {# character} other {# characters}} is recommended to create an informative and enticing summary."
- },
- "project.text-validation.title-game-version": {
- "defaultMessage": "Project titles should not include the Minecraft version “{value}”."
- },
- "project.text-validation.title-loader": {
- "defaultMessage": "Project titles should not include the loader “{value}”."
- }
+ "nags.add-description.description": {
+ "defaultMessage": "A description that clearly describes the project's purpose and function is required."
+ },
+ "nags.add-description.title": {
+ "defaultMessage": "Add a description"
+ },
+ "nags.add-icon.description": {
+ "defaultMessage": "Adding a unique, relevant, and engaging icon makes your project identifiable and helps it stand out."
+ },
+ "nags.add-icon.title": {
+ "defaultMessage": "Add an icon"
+ },
+ "nags.add-java-address.description": {
+ "defaultMessage": "Add the IP address and port Java Edition players can use to join your server."
+ },
+ "nags.add-java-address.title": {
+ "defaultMessage": "Add a Java address"
+ },
+ "nags.add-license-details.description": {
+ "defaultMessage": "Add a valid URL and name or SPDX identifier for your custom license."
+ },
+ "nags.add-license-details.title": {
+ "defaultMessage": "Add license details"
+ },
+ "nags.add-links-server.description": {
+ "defaultMessage": "Add any relevant links targeted outside of Modrinth, such as a website, store, or a Discord invite."
+ },
+ "nags.add-links-server.title": {
+ "defaultMessage": "Add external links"
+ },
+ "nags.add-links.description": {
+ "defaultMessage": "Add any relevant links targeted outside of Modrinth, such as source code, an issue tracker, or a Discord invite."
+ },
+ "nags.add-links.title": {
+ "defaultMessage": "Add external links"
+ },
+ "nags.all-languages.description": {
+ "defaultMessage": "You've selected all available language options. Please list only the languages your server actively supports."
+ },
+ "nags.all-languages.title": {
+ "defaultMessage": "Select accurate languages"
+ },
+ "nags.all-tags-selected.description": {
+ "defaultMessage": "You've selected all {totalAvailableTags, plural, one {# available tag} other {# available tags}}. This defeats the purpose of tags, which are meant to help users find relevant projects. Please select only the tags that are relevant to your project."
+ },
+ "nags.all-tags-selected.title": {
+ "defaultMessage": "Select accurate tags"
+ },
+ "nags.check-disclosures.description": {
+ "defaultMessage": "Make sure users are aware of any important details by filling in content disclosures that apply to your {type}."
+ },
+ "nags.check-disclosures.title": {
+ "defaultMessage": "Check content disclosures"
+ },
+ "nags.description-too-short.description": {
+ "defaultMessage": "Your description is {length, plural, one {# readable character} other {# readable characters}}. At least {minChars, plural, one {# character} other {# characters}} is recommended to create a clear and informative description."
+ },
+ "nags.description-too-short.title": {
+ "defaultMessage": "Expand the description"
+ },
+ "nags.edit-description.title": {
+ "defaultMessage": "Edit description"
+ },
+ "nags.edit-gallery.title": {
+ "defaultMessage": "Edit gallery"
+ },
+ "nags.edit-license.title": {
+ "defaultMessage": "Edit license"
+ },
+ "nags.edit-summary.title": {
+ "defaultMessage": "Edit summary"
+ },
+ "nags.edit-tags.title": {
+ "defaultMessage": "Edit tags"
+ },
+ "nags.edit-title.title": {
+ "defaultMessage": "Edit title"
+ },
+ "nags.feature-gallery-image.description": {
+ "defaultMessage": "The featured gallery image is often how your project makes its first impression."
+ },
+ "nags.feature-gallery-image.title": {
+ "defaultMessage": "Feature a gallery image"
+ },
+ "nags.gallery.title": {
+ "defaultMessage": "Visit gallery page"
+ },
+ "nags.gpl-license-source-required.description": {
+ "defaultMessage": "Your {type} uses a license which requires source code to be available. Please provide a source code link or sources file for each additional version, or consider using a different license."
+ },
+ "nags.gpl-license-source-required.title": {
+ "defaultMessage": "Provide source code"
+ },
+ "nags.identical-links.description": {
+ "defaultMessage": "Some of your external links appear to be identical. Each link should be entered only once and with the appropriate link type."
+ },
+ "nags.identical-links.title": {
+ "defaultMessage": "Clean up identical links"
+ },
+ "nags.image-heavy-description.description": {
+ "defaultMessage": "Your Description should contain sufficient plain text or image alt-text, keeping it accessible to those using screen readers or with slow internet connections."
+ },
+ "nags.image-heavy-description.title": {
+ "defaultMessage": "Ensure accessibility"
+ },
+ "nags.invalid-gallery-text.title": {
+ "defaultMessage": "Fix gallery text"
+ },
+ "nags.invalid-license-url.description.default": {
+ "defaultMessage": "License URL is invalid."
+ },
+ "nags.invalid-license-url.description.domain": {
+ "defaultMessage": "Your license URL points to {domain}, which is not appropriate for license information. License URLs should link directly to your license file, not social media, gaming platforms, etc."
+ },
+ "nags.invalid-license-url.description.malformed": {
+ "defaultMessage": "Your license URL appears to be malformed. Please provide a valid URL to your license text."
+ },
+ "nags.invalid-license-url.title": {
+ "defaultMessage": "Add a valid license link"
+ },
+ "nags.invalid-project-description.title": {
+ "defaultMessage": "Fix the project description"
+ },
+ "nags.invalid-project-name.title": {
+ "defaultMessage": "Fix the project name"
+ },
+ "nags.invalid-project-summary.title": {
+ "defaultMessage": "Fix the project summary"
+ },
+ "nags.link-shortener-usage.description": {
+ "defaultMessage": "Use of link shorteners or other methods to obscure where a link may lead in your external links or license link is prohibited, please only use appropriate full length links."
+ },
+ "nags.link-shortener-usage.title": {
+ "defaultMessage": "Don't use link shorteners"
+ },
+ "nags.link.discord.channel": {
+ "defaultMessage": "This is a link to a Discord channel, not a server invite."
+ },
+ "nags.link.discord.invite.expires": {
+ "defaultMessage": "This Discord invite is set to expire"
+ },
+ "nags.link.discord.invite.invalid": {
+ "defaultMessage": "This Discord invite is invalid or has expired."
+ },
+ "nags.link.discord.invite.not-guild": {
+ "defaultMessage": "This Discord invite does not lead to a server."
+ },
+ "nags.link.discord.user": {
+ "defaultMessage": "This is a link to a Discord user, not a server invite."
+ },
+ "nags.link.expected-type": {
+ "defaultMessage": "This isn't a valid {label} link."
+ },
+ "nags.link.git.archived": {
+ "defaultMessage": "This repository is archived, which disables issues."
+ },
+ "nags.link.git.empty": {
+ "defaultMessage": "This repository appears to be empty."
+ },
+ "nags.link.git.issues-disabled": {
+ "defaultMessage": "Issues are disabled on this repository."
+ },
+ "nags.link.git.not-found": {
+ "defaultMessage": "This repository could not be found (it may be private or deleted)."
+ },
+ "nags.link.git.wiki-disabled": {
+ "defaultMessage": "The wiki is disabled on this repository."
+ },
+ "nags.link.invalid-url": {
+ "defaultMessage": "There's an invalid URL in the description."
+ },
+ "nags.link.license.url-mismatch": {
+ "defaultMessage": "This link points to the {detected} license, but your project is set to {selected}."
+ },
+ "nags.link.license.url-redundant": {
+ "defaultMessage": "You don't need to link to a generic license page for a supported license — consider linking to your repository's own license file instead, or leaving this blank."
+ },
+ "nags.link.never-valid": {
+ "defaultMessage": "{label} links aren't allowed here."
+ },
+ "nags.link.unverifiable-redirect": {
+ "defaultMessage": "This doesn't look like a {platform} link."
+ },
+ "nags.link.wrong-field": {
+ "defaultMessage": "{label} links aren't valid for this field."
+ },
+ "nags.link.youtube.unrecognized": {
+ "defaultMessage": "This doesn't look like a YouTube donation link."
+ },
+ "nags.long-headers.description": {
+ "defaultMessage": "{count, plural, one {# header} other {# headers}} in your description {count, plural, one {is} other {are}} too long. Headers should be concise and act as section titles, not full sentences."
+ },
+ "nags.long-headers.title": {
+ "defaultMessage": "Shorten headers"
+ },
+ "nags.minecraft-title-clause.description": {
+ "defaultMessage": "Projects must not use Minecraft's branding or include \"Minecraft\" as a significant part of the name."
+ },
+ "nags.minecraft-title-clause.title": {
+ "defaultMessage": "Avoid brand infringement"
+ },
+ "nags.missing-alt-text.description": {
+ "defaultMessage": "Some of your images are missing alt text, which is important for accessibility, especially for visually impaired users."
+ },
+ "nags.missing-alt-text.title": {
+ "defaultMessage": "Add image alt text"
+ },
+ "nags.misused-discord-link-description": {
+ "defaultMessage": "Discord invites can not be used for other link types. Please put your Discord link in the Discord Invite link field only."
+ },
+ "nags.misused-discord-link.title": {
+ "defaultMessage": "Move Discord invite"
+ },
+ "nags.moderation.title": {
+ "defaultMessage": "Visit moderation thread"
+ },
+ "nags.moderator-feedback.description": {
+ "defaultMessage": "Review and address all concerns from the moderation team before resubmitting."
+ },
+ "nags.moderator-feedback.title": {
+ "defaultMessage": "Review feedback"
+ },
+ "nags.multiple-resolution-tags.description": {
+ "defaultMessage": "You've selected {count, plural, one {# resolution tag} other {# resolution tags}} ({tags}). Resource packs should typically only have one resolution tag that matches their primary resolution."
+ },
+ "nags.multiple-resolution-tags.title": {
+ "defaultMessage": "Select correct resolution"
+ },
+ "nags.project-name-metadata.title": {
+ "defaultMessage": "Remove technical details from the name"
+ },
+ "nags.project-summary-content.title": {
+ "defaultMessage": "Review the project summary"
+ },
+ "nags.review-permissions.description": {
+ "defaultMessage": "Make sure you have provided proof of your permission to distribute any external content in your Modpack."
+ },
+ "nags.review-permissions.title": {
+ "defaultMessage": "Review external permissions"
+ },
+ "nags.select-compatibility.description": {
+ "defaultMessage": "Select what versions your server supports, choose a Modpack, or upload your own."
+ },
+ "nags.select-compatibility.title": {
+ "defaultMessage": "Select compatibility"
+ },
+ "nags.select-country.description": {
+ "defaultMessage": "Let players know what region your server is located in."
+ },
+ "nags.select-country.title": {
+ "defaultMessage": "Select a region"
+ },
+ "nags.select-language.description": {
+ "defaultMessage": "List the language or languages supported by your server."
+ },
+ "nags.select-language.title": {
+ "defaultMessage": "Select a language"
+ },
+ "nags.select-license.description": {
+ "defaultMessage": "Select the license your {type} is distributed under."
+ },
+ "nags.select-license.title": {
+ "defaultMessage": "Select a license"
+ },
+ "nags.select-tags.description": {
+ "defaultMessage": "Select the tags that correctly apply to your project to help the right users find it."
+ },
+ "nags.select-tags.title": {
+ "defaultMessage": "Select tags"
+ },
+ "nags.server.title": {
+ "defaultMessage": "Visit server settings"
+ },
+ "nags.settings.description.title": {
+ "defaultMessage": "Visit description settings"
+ },
+ "nags.settings.disclosures.title": {
+ "defaultMessage": "Visit disclosure settings"
+ },
+ "nags.settings.license.title": {
+ "defaultMessage": "Visit license settings"
+ },
+ "nags.settings.links.title": {
+ "defaultMessage": "Visit links settings"
+ },
+ "nags.settings.permissions.title": {
+ "defaultMessage": "Visit permissions dashboard"
+ },
+ "nags.settings.tags.title": {
+ "defaultMessage": "Visit tag settings"
+ },
+ "nags.settings.title": {
+ "defaultMessage": "Visit general settings"
+ },
+ "nags.settings.versions.title": {
+ "defaultMessage": "Visit versions settings"
+ },
+ "nags.summary-special-formatting.description": {
+ "defaultMessage": "Your summary should not contain formatting, line breaks, or special characters, since the summary will only display plain text."
+ },
+ "nags.summary-special-formatting.title": {
+ "defaultMessage": "Clean up the summary"
+ },
+ "nags.summary-too-short.title": {
+ "defaultMessage": "Expand the summary"
+ },
+ "nags.too-many-languages.description": {
+ "defaultMessage": "You've selected {languageCount, plural, one {# language} other {# languages}}. Please list only the languages your server actively supports."
+ },
+ "nags.too-many-languages.title": {
+ "defaultMessage": "Select accurate languages"
+ },
+ "nags.too-many-tags-server.description": {
+ "defaultMessage": "You've selected {tagCount, plural, one {# tag} other {# tags}}. Please reduce to {maxTagCount} or fewer to make sure your server appears in relevant search results."
+ },
+ "nags.too-many-tags-server.title": {
+ "defaultMessage": "Select accurate tags"
+ },
+ "nags.too-many-tags.description": {
+ "defaultMessage": "You've selected {tagCount, plural, one {# tag} other {# tags}}. Consider reducing to {maxTagCount} or fewer to make sure your project appears in relevant search results."
+ },
+ "nags.too-many-tags.title": {
+ "defaultMessage": "Select accurate tags"
+ },
+ "nags.upload-gallery-image.description": {
+ "defaultMessage": "At least one gallery image is required to showcase the content of your {type}."
+ },
+ "nags.upload-gallery-image.description-resourcepack": {
+ "defaultMessage": "At least one gallery image is required to showcase the content of your resource pack, except for audio or localization packs. If this describes your pack, please select the appropriate tag."
+ },
+ "nags.upload-gallery-image.description-shader": {
+ "defaultMessage": "At least three gallery images are required to showcase the content of your shader in a variety of situations and conditions."
+ },
+ "nags.upload-gallery-image.title": {
+ "defaultMessage": "Upload a gallery image"
+ },
+ "nags.upload-version.description": {
+ "defaultMessage": "At least one version is required for a project to be submitted for review."
+ },
+ "nags.upload-version.title": {
+ "defaultMessage": "Upload a version"
+ },
+ "nags.verify-external-links.description": {
+ "defaultMessage": "Some of your external links may be using domains that are inappropriate for that type of link."
+ },
+ "nags.verify-external-links.title": {
+ "defaultMessage": "Verify external links"
+ },
+ "nags.visit-links-settings.title": {
+ "defaultMessage": "Visit links settings"
+ },
+ "project.text-validation.description-profanity": {
+ "defaultMessage": "Excessive profanity is not allowed. Detected: {values}."
+ },
+ "project.text-validation.non-standard-text": {
+ "defaultMessage": "Non-standard text characters are not allowed."
+ },
+ "project.text-validation.profanity": {
+ "defaultMessage": "The detected profanity “{value}” is not allowed."
+ },
+ "project.text-validation.slur": {
+ "defaultMessage": "The detected slur “{value}” is not allowed."
+ },
+ "project.text-validation.summary-link": {
+ "defaultMessage": "Links should not be included in project summaries."
+ },
+ "project.text-validation.summary-matches-title": {
+ "defaultMessage": "A project summary should not be the same as its title."
+ },
+ "project.text-validation.summary-too-short": {
+ "defaultMessage": "Your summary is {length, plural, one {# character} other {# characters}}. At least {minChars, plural, one {# character} other {# characters}} is recommended to create an informative and enticing summary."
+ },
+ "project.text-validation.title-game-version": {
+ "defaultMessage": "Project titles should not include the Minecraft version “{value}”."
+ },
+ "project.text-validation.title-loader": {
+ "defaultMessage": "Project titles should not include the loader “{value}”."
+ }
}
diff --git a/packages/moderation/src/validators/profanity/index.ts b/packages/moderation/src/validators/profanity/index.ts
index 0a0db37799..1f7bbac132 100644
--- a/packages/moderation/src/validators/profanity/index.ts
+++ b/packages/moderation/src/validators/profanity/index.ts
@@ -248,6 +248,44 @@ function isWordCharacter(character: string | undefined): boolean {
return character !== undefined && /^[\p{L}\p{M}\p{N}_]$/u.test(character)
}
+const LEET_SPEAK_CHARACTERS = new Set(['@', '(', '|', '!', '/', '$'])
+
+function isObfuscatedWordCharacter(character: string): boolean {
+ return isWordCharacter(character) || LEET_SPEAK_CHARACTERS.has(character)
+}
+
+function isCharacterByCharacterObfuscation(text: string): boolean {
+ const chunkLengths: number[] = []
+ let currentChunkLength = 0
+ let hasInvisibleSeparator = false
+ let hasVisibleSeparator = false
+
+ for (const character of text) {
+ if (isObfuscatedWordCharacter(character)) {
+ currentChunkLength++
+ } else {
+ if (/^\p{Cf}$/u.test(character)) {
+ hasInvisibleSeparator = true
+ } else {
+ hasVisibleSeparator = true
+ }
+
+ if (currentChunkLength > 0) {
+ chunkLengths.push(currentChunkLength)
+ currentChunkLength = 0
+ }
+ }
+ }
+
+ if (currentChunkLength > 0) chunkLengths.push(currentChunkLength)
+
+ return (
+ chunkLengths.length > 1 &&
+ ((hasInvisibleSeparator && !hasVisibleSeparator) ||
+ chunkLengths.every((length) => length === 1))
+ )
+}
+
function getCharacterBefore(text: string, index: number): string | undefined {
if (index <= 0) return undefined
@@ -282,27 +320,45 @@ export function createProfanityValidator(
return { kind: pattern.kind, term }
})
- const matcher = new RegExpMatcher({
- blacklistedTerms: entries.map(({ term }, id) => ({ id, pattern: parseRawPattern(term) })),
+ const blacklistedTerms = entries.map(({ term }, id) => ({ id, pattern: parseRawPattern(term) }))
+ const baseTransformers = [
+ resolveConfusablesTransformer(),
+ resolveLeetSpeakTransformer(),
+ toAsciiLowerCaseTransformer(),
+ ]
+ const duplicateTransformer = () =>
+ collapseDuplicatesTransformer({
+ customThresholds: getDuplicateThresholds(entries.map(({ term }) => term)),
+ })
+ const strictMatcher = new RegExpMatcher({
+ blacklistedTerms,
+ blacklistMatcherTransformers: [...baseTransformers, duplicateTransformer()],
+ })
+ const separatorMatcher = new RegExpMatcher({
+ blacklistedTerms,
blacklistMatcherTransformers: [
- resolveConfusablesTransformer(),
- resolveLeetSpeakTransformer(),
- toAsciiLowerCaseTransformer(),
+ ...baseTransformers,
skipNonAlphabeticTransformer(),
- collapseDuplicatesTransformer({
- customThresholds: getDuplicateThresholds(entries.map(({ term }) => term)),
- }),
+ duplicateTransformer(),
],
})
function findAll(text: string): ProfanityMatch[] {
const matches: ProfanityMatch[] = []
+ const strictMatches = new Set(
+ [...strictMatcher.getAllMatches(text, true)].map(
+ (match) => `${match.termId}:${match.startIndex}:${match.endIndex}`,
+ ),
+ )
- for (const match of matcher.getAllMatches(text, true)) {
+ for (const match of separatorMatcher.getAllMatches(text, true)) {
const profanityPattern = entries[match.termId]
const end = match.endIndex + 1
+ const rawText = text.slice(match.startIndex, end)
+ const matchKey = `${match.termId}:${match.startIndex}:${match.endIndex}`
if (
!profanityPattern ||
+ (!strictMatches.has(matchKey) && !isCharacterByCharacterObfuscation(rawText)) ||
!isWholeWordMatch(text, match.startIndex, end) ||
match.startIndex < (matches.at(-1)?.end ?? 0)
) {
@@ -311,7 +367,7 @@ export function createProfanityValidator(
matches.push({
...profanityPattern,
- rawText: text.slice(match.startIndex, end),
+ rawText,
start: match.startIndex,
end,
})
diff --git a/packages/moderation/src/validators/profanity/tests.ts b/packages/moderation/src/validators/profanity/tests.ts
index 0e1be26f4c..21d9fc4bac 100644
--- a/packages/moderation/src/validators/profanity/tests.ts
+++ b/packages/moderation/src/validators/profanity/tests.ts
@@ -52,6 +52,11 @@ test('does not join ordinary words or match inside larger words', () => {
assert.equal(validateProfanity('s e m e n').valid, false)
})
+test('does not join multi-character chunks into a slur', () => {
+ assert.equal(validateProfanity('6000 -> OK').valid, true)
+ assert.equal(validateProfanity('6000 OK').valid, true)
+})
+
test('rejects any uncensored configured profanity', () => {
assert.equal(validateProfanity('A clean project').valid, true)
assert.equal(validateProfanity('This is shit').valid, false)
diff --git a/packages/moderation/src/validators/project-fields/index.ts b/packages/moderation/src/validators/project-fields/index.ts
index 64e9bfa344..798ec96e94 100644
--- a/packages/moderation/src/validators/project-fields/index.ts
+++ b/packages/moderation/src/validators/project-fields/index.ts
@@ -60,6 +60,10 @@ const messages = defineMessages({
id: 'project.text-validation.profanity',
defaultMessage: 'The detected profanity “{value}” is not allowed.',
},
+ descriptionProfanity: {
+ id: 'project.text-validation.description-profanity',
+ defaultMessage: 'Excessive profanity is not allowed. Detected: {values}',
+ },
nonStandardText: {
id: 'project.text-validation.non-standard-text',
defaultMessage: 'Non-standard text characters are not allowed.',
@@ -398,6 +402,20 @@ export function validateProjectDescription(
maxProfanityCount: DESCRIPTION_MAX_PROFANITY_COUNT,
nonStandardTextFailureThreshold: DESCRIPTION_NON_STANDARD_TEXT_FAILURE_THRESHOLD,
})
+ if (results[0]?.code === 'text-profanity') {
+ const detectedValues = validateProfanity(description ?? '')
+ .matches.filter((match) => match.kind === 'profanity')
+ .map((match) => `"${match.rawText}"`)
+ .join(', ')
+
+ return [
+ {
+ ...results[0],
+ message: messages.descriptionProfanity,
+ values: { values: detectedValues },
+ },
+ ]
+ }
if (results.length > 0) return results
const normalizedDescription = normalizeProjectFieldText(description ?? '')
diff --git a/packages/moderation/src/validators/project-fields/tests.ts b/packages/moderation/src/validators/project-fields/tests.ts
index 92108d6d5d..05f169e9dd 100644
--- a/packages/moderation/src/validators/project-fields/tests.ts
+++ b/packages/moderation/src/validators/project-fields/tests.ts
@@ -158,10 +158,13 @@ test('allows one profanity match in descriptions but rejects a second match or a
code: 'text-profanity',
severity: 'error',
message: {
- id: 'project.text-validation.profanity',
- defaultMessage: 'The detected profanity “{value}” is not allowed.',
+ id: 'project.text-validation.description-profanity',
+ defaultMessage: 'Excessive profanity is not allowed. Detected: {values}',
},
- values: { value: 'FUCK' },
+ values: { values: '"shit", "FUCK"' },
+ })
+ assert.deepEqual(validateProjectDescription(`${description} shit FUCK bastard`)[0]?.values, {
+ values: '"shit", "FUCK", "bastard"',
})
assert.equal(validateProjectDescription(`${description} nigga`)[0]?.code, 'text-slur')
assert.equal(validateProjectDescription(`${description} nigger`)[0]?.code, 'text-slur')