fix: less false positives in profanity and fix gallary nag

This commit is contained in:
tdgao
2026-08-26 16:55:29 -06:00
parent 31e405e73c
commit b1d33373fe
8 changed files with 489 additions and 420 deletions
@@ -92,7 +92,7 @@
v-if="editIndex === -1" v-if="editIndex === -1"
type="colored" type="colored"
color="brand" color="brand"
:disabled="shouldPreventActions || galleryFieldsInvalid" :disabled="shouldPreventActions || !canSaveGalleryFields"
@click="createGalleryItem" @click="createGalleryItem"
> >
<PlusIcon aria-hidden="true" /> <PlusIcon aria-hidden="true" />
@@ -102,7 +102,7 @@
v-else v-else
type="colored" type="colored"
color="brand" color="brand"
:disabled="shouldPreventActions || galleryFieldsInvalid" :disabled="shouldPreventActions || !canSaveGalleryFields"
@click="editGalleryItem" @click="editGalleryItem"
> >
<SaveIcon aria-hidden="true" /> <SaveIcon aria-hidden="true" />
@@ -312,6 +312,7 @@ import {
Textarea, Textarea,
useFormatDateTime, useFormatDateTime,
} from '@modrinth/ui' } from '@modrinth/ui'
import { isAdmin } from '@modrinth/utils'
import { useEventListener } from '@vueuse/core' import { useEventListener } from '@vueuse/core'
import AiImageWarningModal from '~/components/ui/AiImageWarningModal.vue' import AiImageWarningModal from '~/components/ui/AiImageWarningModal.vue'
@@ -390,6 +391,8 @@ const galleryFieldsInvalid = computed(
galleryTitleValidation.value.some((validation) => validation.severity === 'error') || galleryTitleValidation.value.some((validation) => validation.severity === 'error') ||
galleryDescriptionValidation.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 // Constant for accepted file types
const MC_SERVER_BANNER_NAME = '__mc_server_banner__' const MC_SERVER_BANNER_NAME = '__mc_server_banner__'
@@ -490,7 +493,7 @@ function showPreviewImage() {
// CRUD operations // CRUD operations
async function createGalleryItem() { async function createGalleryItem() {
if (galleryFieldsInvalid.value) return if (!canSaveGalleryFields.value) return
shouldPreventActions.value = true shouldPreventActions.value = true
startLoading() startLoading()
@@ -511,7 +514,7 @@ async function createGalleryItem() {
} }
async function editGalleryItem() { async function editGalleryItem() {
if (galleryFieldsInvalid.value) return if (!canSaveGalleryFields.value) return
shouldPreventActions.value = true shouldPreventActions.value = true
startLoading() startLoading()
@@ -92,7 +92,7 @@
v-if="editIndex === -1" v-if="editIndex === -1"
type="colored" type="colored"
color="brand" color="brand"
:disabled="shouldPreventActions || galleryFieldsInvalid" :disabled="shouldPreventActions || !canSaveGalleryFields"
@click="createGalleryItem" @click="createGalleryItem"
> >
<PlusIcon aria-hidden="true" /> <PlusIcon aria-hidden="true" />
@@ -102,7 +102,7 @@
v-else v-else
type="colored" type="colored"
color="brand" color="brand"
:disabled="shouldPreventActions || galleryFieldsInvalid" :disabled="shouldPreventActions || !canSaveGalleryFields"
@click="editGalleryItem" @click="editGalleryItem"
> >
<SaveIcon aria-hidden="true" /> <SaveIcon aria-hidden="true" />
@@ -303,6 +303,7 @@ import {
Textarea, Textarea,
useFormatDateTime, useFormatDateTime,
} from '@modrinth/ui' } from '@modrinth/ui'
import { isAdmin } from '@modrinth/utils'
import AiImageWarningModal from '~/components/ui/AiImageWarningModal.vue' import AiImageWarningModal from '~/components/ui/AiImageWarningModal.vue'
import ValidationMessage from '~/components/ValidationMessage.vue' import ValidationMessage from '~/components/ValidationMessage.vue'
@@ -350,6 +351,8 @@ const galleryFieldsInvalid = computed(
galleryTitleValidation.value.some((validation) => validation.severity === 'error') || galleryTitleValidation.value.some((validation) => validation.severity === 'error') ||
galleryDescriptionValidation.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 MC_SERVER_BANNER_NAME = '__mc_server_banner__'
const acceptFileTypes = 'image/png,image/jpeg,image/gif,image/webp,.png,.jpeg,.gif,.webp' const acceptFileTypes = 'image/png,image/jpeg,image/gif,image/webp,.png,.jpeg,.gif,.webp'
@@ -430,7 +433,7 @@ const showPreviewImage = () => {
} }
const createGalleryItem = async () => { const createGalleryItem = async () => {
if (galleryFieldsInvalid.value) return if (!canSaveGalleryFields.value) return
shouldPreventActions.value = true shouldPreventActions.value = true
const success = await createGalleryItemMutation( const success = await createGalleryItemMutation(
@@ -449,7 +452,7 @@ const createGalleryItem = async () => {
} }
const editGalleryItem = async () => { const editGalleryItem = async () => {
if (galleryFieldsInvalid.value) return if (!canSaveGalleryFields.value) return
shouldPreventActions.value = true shouldPreventActions.value = true
const success = await editGalleryItemMutation( const success = await editGalleryItemMutation(
@@ -33,14 +33,6 @@ const descriptionErrorCodes: readonly ProjectTextValidationCode[] = [
'text-non-standard', '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( function getFirstFailure(
context: NagContext, context: NagContext,
fields: readonly ProjectValidationField[], fields: readonly ProjectValidationField[],
@@ -80,18 +72,6 @@ function getCodedFailureDescription(context: NagContext, code: ProjectTextValida
return formatMessage(failure.message, failure.values) 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[] = [ export const projectValidationNags: Nag[] = [
{ {
id: 'invalid-project-name', id: 'invalid-project-name',
@@ -341,7 +321,8 @@ export const projectValidationNags: Nag[] = [
id: 'nags.invalid-gallery-text.title', id: 'nags.invalid-gallery-text.title',
defaultMessage: 'Fix gallery text', defaultMessage: 'Fix gallery text',
}), }),
description: getGalleryFailureDescription, description: (context) =>
getFailureDescription(context, ['gallery-name', 'gallery-description'], 'error'),
status: 'required', status: 'required',
shouldShow: (context) => shouldShow: (context) =>
getFirstFailure(context, ['gallery-name', 'gallery-description'], 'error') !== undefined, getFirstFailure(context, ['gallery-name', 'gallery-description'], 'error') !== undefined,
+378 -378
View File
@@ -1,380 +1,380 @@
{ {
"nags.add-description.description": { "nags.add-description.description": {
"defaultMessage": "A description that clearly describes the project's purpose and function is required." "defaultMessage": "A description that clearly describes the project's purpose and function is required."
}, },
"nags.add-description.title": { "nags.add-description.title": {
"defaultMessage": "Add a description" "defaultMessage": "Add a description"
}, },
"nags.add-icon.description": { "nags.add-icon.description": {
"defaultMessage": "Adding a unique, relevant, and engaging icon makes your project identifiable and helps it stand out." "defaultMessage": "Adding a unique, relevant, and engaging icon makes your project identifiable and helps it stand out."
}, },
"nags.add-icon.title": { "nags.add-icon.title": {
"defaultMessage": "Add an icon" "defaultMessage": "Add an icon"
}, },
"nags.add-java-address.description": { "nags.add-java-address.description": {
"defaultMessage": "Add the IP address and port Java Edition players can use to join your server." "defaultMessage": "Add the IP address and port Java Edition players can use to join your server."
}, },
"nags.add-java-address.title": { "nags.add-java-address.title": {
"defaultMessage": "Add a Java address" "defaultMessage": "Add a Java address"
}, },
"nags.add-license-details.description": { "nags.add-license-details.description": {
"defaultMessage": "Add a valid URL and name or SPDX identifier for your custom license." "defaultMessage": "Add a valid URL and name or SPDX identifier for your custom license."
}, },
"nags.add-license-details.title": { "nags.add-license-details.title": {
"defaultMessage": "Add license details" "defaultMessage": "Add license details"
}, },
"nags.add-links-server.description": { "nags.add-links-server.description": {
"defaultMessage": "Add any relevant links targeted outside of Modrinth, such as a website, store, or a Discord invite." "defaultMessage": "Add any relevant links targeted outside of Modrinth, such as a website, store, or a Discord invite."
}, },
"nags.add-links-server.title": { "nags.add-links-server.title": {
"defaultMessage": "Add external links" "defaultMessage": "Add external links"
}, },
"nags.add-links.description": { "nags.add-links.description": {
"defaultMessage": "Add any relevant links targeted outside of Modrinth, such as source code, an issue tracker, or a Discord invite." "defaultMessage": "Add any relevant links targeted outside of Modrinth, such as source code, an issue tracker, or a Discord invite."
}, },
"nags.add-links.title": { "nags.add-links.title": {
"defaultMessage": "Add external links" "defaultMessage": "Add external links"
}, },
"nags.all-languages.description": { "nags.all-languages.description": {
"defaultMessage": "You've selected all available language options. Please list only the languages your server actively supports." "defaultMessage": "You've selected all available language options. Please list only the languages your server actively supports."
}, },
"nags.all-languages.title": { "nags.all-languages.title": {
"defaultMessage": "Select accurate languages" "defaultMessage": "Select accurate languages"
}, },
"nags.all-tags-selected.description": { "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." "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": { "nags.all-tags-selected.title": {
"defaultMessage": "Select accurate tags" "defaultMessage": "Select accurate tags"
}, },
"nags.check-disclosures.description": { "nags.check-disclosures.description": {
"defaultMessage": "Make sure users are aware of any important details by filling in content disclosures that apply to your {type}." "defaultMessage": "Make sure users are aware of any important details by filling in content disclosures that apply to your {type}."
}, },
"nags.check-disclosures.title": { "nags.check-disclosures.title": {
"defaultMessage": "Check content disclosures" "defaultMessage": "Check content disclosures"
}, },
"nags.description-too-short.description": { "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." "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": { "nags.description-too-short.title": {
"defaultMessage": "Expand the description" "defaultMessage": "Expand the description"
}, },
"nags.edit-description.title": { "nags.edit-description.title": {
"defaultMessage": "Edit description" "defaultMessage": "Edit description"
}, },
"nags.edit-gallery.title": { "nags.edit-gallery.title": {
"defaultMessage": "Edit gallery" "defaultMessage": "Edit gallery"
}, },
"nags.edit-license.title": { "nags.edit-license.title": {
"defaultMessage": "Edit license" "defaultMessage": "Edit license"
}, },
"nags.edit-summary.title": { "nags.edit-summary.title": {
"defaultMessage": "Edit summary" "defaultMessage": "Edit summary"
}, },
"nags.edit-tags.title": { "nags.edit-tags.title": {
"defaultMessage": "Edit tags" "defaultMessage": "Edit tags"
}, },
"nags.edit-title.title": { "nags.edit-title.title": {
"defaultMessage": "Edit title" "defaultMessage": "Edit title"
}, },
"nags.feature-gallery-image.description": { "nags.feature-gallery-image.description": {
"defaultMessage": "The featured gallery image is often how your project makes its first impression." "defaultMessage": "The featured gallery image is often how your project makes its first impression."
}, },
"nags.feature-gallery-image.title": { "nags.feature-gallery-image.title": {
"defaultMessage": "Feature a gallery image" "defaultMessage": "Feature a gallery image"
}, },
"nags.gallery.title": { "nags.gallery.title": {
"defaultMessage": "Visit gallery page" "defaultMessage": "Visit gallery page"
}, },
"nags.gpl-license-source-required.description": { "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." "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": { "nags.gpl-license-source-required.title": {
"defaultMessage": "Provide source code" "defaultMessage": "Provide source code"
}, },
"nags.identical-links.description": { "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." "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": { "nags.identical-links.title": {
"defaultMessage": "Clean up identical links" "defaultMessage": "Clean up identical links"
}, },
"nags.image-heavy-description.description": { "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." "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": { "nags.image-heavy-description.title": {
"defaultMessage": "Ensure accessibility" "defaultMessage": "Ensure accessibility"
}, },
"nags.invalid-gallery-text.description": { "nags.invalid-gallery-text.title": {
"defaultMessage": "Gallery image {number} {field, select, gallery-name {name} gallery-description {description} other {text}}: {error}" "defaultMessage": "Fix gallery text"
}, },
"nags.invalid-gallery-text.title": { "nags.invalid-license-url.description.default": {
"defaultMessage": "Fix gallery text" "defaultMessage": "License URL is invalid."
}, },
"nags.invalid-license-url.description.default": { "nags.invalid-license-url.description.domain": {
"defaultMessage": "License URL is invalid." "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.domain": { "nags.invalid-license-url.description.malformed": {
"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." "defaultMessage": "Your license URL appears to be malformed. Please provide a valid URL to your license text."
}, },
"nags.invalid-license-url.description.malformed": { "nags.invalid-license-url.title": {
"defaultMessage": "Your license URL appears to be malformed. Please provide a valid URL to your license text." "defaultMessage": "Add a valid license link"
}, },
"nags.invalid-license-url.title": { "nags.invalid-project-description.title": {
"defaultMessage": "Add a valid license link" "defaultMessage": "Fix the project description"
}, },
"nags.invalid-project-description.title": { "nags.invalid-project-name.title": {
"defaultMessage": "Fix the project description" "defaultMessage": "Fix the project name"
}, },
"nags.invalid-project-name.title": { "nags.invalid-project-summary.title": {
"defaultMessage": "Fix the project name" "defaultMessage": "Fix the project summary"
}, },
"nags.invalid-project-summary.title": { "nags.link-shortener-usage.description": {
"defaultMessage": "Fix the project summary" "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.description": { "nags.link-shortener-usage.title": {
"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." "defaultMessage": "Don't use link shorteners"
}, },
"nags.link-shortener-usage.title": { "nags.link.discord.channel": {
"defaultMessage": "Don't use link shorteners" "defaultMessage": "This is a link to a Discord channel, not a server invite."
}, },
"nags.link.discord.channel": { "nags.link.discord.invite.expires": {
"defaultMessage": "This is a link to a Discord channel, not a server invite." "defaultMessage": "This Discord invite is set to expire"
}, },
"nags.link.discord.invite.expires": { "nags.link.discord.invite.invalid": {
"defaultMessage": "This Discord invite is set to expire" "defaultMessage": "This Discord invite is invalid or has expired."
}, },
"nags.link.discord.invite.invalid": { "nags.link.discord.invite.not-guild": {
"defaultMessage": "This Discord invite is invalid or has expired." "defaultMessage": "This Discord invite does not lead to a server."
}, },
"nags.link.discord.invite.not-guild": { "nags.link.discord.user": {
"defaultMessage": "This Discord invite does not lead to a server." "defaultMessage": "This is a link to a Discord user, not a server invite."
}, },
"nags.link.discord.user": { "nags.link.expected-type": {
"defaultMessage": "This is a link to a Discord user, not a server invite." "defaultMessage": "This isn't a valid {label} link."
}, },
"nags.link.expected-type": { "nags.link.git.archived": {
"defaultMessage": "This isn't a valid {label} link." "defaultMessage": "This repository is archived, which disables issues."
}, },
"nags.link.git.archived": { "nags.link.git.empty": {
"defaultMessage": "This repository is archived, which disables issues." "defaultMessage": "This repository appears to be empty."
}, },
"nags.link.git.empty": { "nags.link.git.issues-disabled": {
"defaultMessage": "This repository appears to be empty." "defaultMessage": "Issues are disabled on this repository."
}, },
"nags.link.git.issues-disabled": { "nags.link.git.not-found": {
"defaultMessage": "Issues are disabled on this repository." "defaultMessage": "This repository could not be found (it may be private or deleted)."
}, },
"nags.link.git.not-found": { "nags.link.git.wiki-disabled": {
"defaultMessage": "This repository could not be found (it may be private or deleted)." "defaultMessage": "The wiki is disabled on this repository."
}, },
"nags.link.git.wiki-disabled": { "nags.link.invalid-url": {
"defaultMessage": "The wiki is disabled on this repository." "defaultMessage": "There's an invalid URL in the description."
}, },
"nags.link.invalid-url": { "nags.link.license.url-mismatch": {
"defaultMessage": "There's an invalid URL in the description." "defaultMessage": "This link points to the {detected} license, but your project is set to {selected}."
}, },
"nags.link.license.url-mismatch": { "nags.link.license.url-redundant": {
"defaultMessage": "This link points to the {detected} license, but your project is set to {selected}." "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.license.url-redundant": { "nags.link.never-valid": {
"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." "defaultMessage": "{label} links aren't allowed here."
}, },
"nags.link.never-valid": { "nags.link.unverifiable-redirect": {
"defaultMessage": "{label} links aren't allowed here." "defaultMessage": "This doesn't look like a {platform} link."
}, },
"nags.link.unverifiable-redirect": { "nags.link.wrong-field": {
"defaultMessage": "This doesn't look like a {platform} link." "defaultMessage": "{label} links aren't valid for this field."
}, },
"nags.link.wrong-field": { "nags.link.youtube.unrecognized": {
"defaultMessage": "{label} links aren't valid for this field." "defaultMessage": "This doesn't look like a YouTube donation link."
}, },
"nags.link.youtube.unrecognized": { "nags.long-headers.description": {
"defaultMessage": "This doesn't look like a YouTube donation link." "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.description": { "nags.long-headers.title": {
"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." "defaultMessage": "Shorten headers"
}, },
"nags.long-headers.title": { "nags.minecraft-title-clause.description": {
"defaultMessage": "Shorten headers" "defaultMessage": "Projects must not use Minecraft's branding or include \"Minecraft\" as a significant part of the name."
}, },
"nags.minecraft-title-clause.description": { "nags.minecraft-title-clause.title": {
"defaultMessage": "Projects must not use Minecraft's branding or include \"Minecraft\" as a significant part of the name." "defaultMessage": "Avoid brand infringement"
}, },
"nags.minecraft-title-clause.title": { "nags.missing-alt-text.description": {
"defaultMessage": "Avoid brand infringement" "defaultMessage": "Some of your images are missing alt text, which is important for accessibility, especially for visually impaired users."
}, },
"nags.missing-alt-text.description": { "nags.missing-alt-text.title": {
"defaultMessage": "Some of your images are missing alt text, which is important for accessibility, especially for visually impaired users." "defaultMessage": "Add image alt text"
}, },
"nags.missing-alt-text.title": { "nags.misused-discord-link-description": {
"defaultMessage": "Add image alt text" "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-description": { "nags.misused-discord-link.title": {
"defaultMessage": "Discord invites can not be used for other link types. Please put your Discord link in the Discord Invite link field only." "defaultMessage": "Move Discord invite"
}, },
"nags.misused-discord-link.title": { "nags.moderation.title": {
"defaultMessage": "Move Discord invite" "defaultMessage": "Visit moderation thread"
}, },
"nags.moderation.title": { "nags.moderator-feedback.description": {
"defaultMessage": "Visit moderation thread" "defaultMessage": "Review and address all concerns from the moderation team before resubmitting."
}, },
"nags.moderator-feedback.description": { "nags.moderator-feedback.title": {
"defaultMessage": "Review and address all concerns from the moderation team before resubmitting." "defaultMessage": "Review feedback"
}, },
"nags.moderator-feedback.title": { "nags.multiple-resolution-tags.description": {
"defaultMessage": "Review feedback" "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.description": { "nags.multiple-resolution-tags.title": {
"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." "defaultMessage": "Select correct resolution"
}, },
"nags.multiple-resolution-tags.title": { "nags.project-name-metadata.title": {
"defaultMessage": "Select correct resolution" "defaultMessage": "Remove technical details from the name"
}, },
"nags.project-name-metadata.title": { "nags.project-summary-content.title": {
"defaultMessage": "Remove technical details from the name" "defaultMessage": "Review the project summary"
}, },
"nags.project-summary-content.title": { "nags.review-permissions.description": {
"defaultMessage": "Review the project summary" "defaultMessage": "Make sure you have provided proof of your permission to distribute any external content in your Modpack."
}, },
"nags.review-permissions.description": { "nags.review-permissions.title": {
"defaultMessage": "Make sure you have provided proof of your permission to distribute any external content in your Modpack." "defaultMessage": "Review external permissions"
}, },
"nags.review-permissions.title": { "nags.select-compatibility.description": {
"defaultMessage": "Review external permissions" "defaultMessage": "Select what versions your server supports, choose a Modpack, or upload your own."
}, },
"nags.select-compatibility.description": { "nags.select-compatibility.title": {
"defaultMessage": "Select what versions your server supports, choose a Modpack, or upload your own." "defaultMessage": "Select compatibility"
}, },
"nags.select-compatibility.title": { "nags.select-country.description": {
"defaultMessage": "Select compatibility" "defaultMessage": "Let players know what region your server is located in."
}, },
"nags.select-country.description": { "nags.select-country.title": {
"defaultMessage": "Let players know what region your server is located in." "defaultMessage": "Select a region"
}, },
"nags.select-country.title": { "nags.select-language.description": {
"defaultMessage": "Select a region" "defaultMessage": "List the language or languages supported by your server."
}, },
"nags.select-language.description": { "nags.select-language.title": {
"defaultMessage": "List the language or languages supported by your server." "defaultMessage": "Select a language"
}, },
"nags.select-language.title": { "nags.select-license.description": {
"defaultMessage": "Select a language" "defaultMessage": "Select the license your {type} is distributed under."
}, },
"nags.select-license.description": { "nags.select-license.title": {
"defaultMessage": "Select the license your {type} is distributed under." "defaultMessage": "Select a license"
}, },
"nags.select-license.title": { "nags.select-tags.description": {
"defaultMessage": "Select a license" "defaultMessage": "Select the tags that correctly apply to your project to help the right users find it."
}, },
"nags.select-tags.description": { "nags.select-tags.title": {
"defaultMessage": "Select the tags that correctly apply to your project to help the right users find it." "defaultMessage": "Select tags"
}, },
"nags.select-tags.title": { "nags.server.title": {
"defaultMessage": "Select tags" "defaultMessage": "Visit server settings"
}, },
"nags.server.title": { "nags.settings.description.title": {
"defaultMessage": "Visit server settings" "defaultMessage": "Visit description settings"
}, },
"nags.settings.description.title": { "nags.settings.disclosures.title": {
"defaultMessage": "Visit description settings" "defaultMessage": "Visit disclosure settings"
}, },
"nags.settings.disclosures.title": { "nags.settings.license.title": {
"defaultMessage": "Visit disclosure settings" "defaultMessage": "Visit license settings"
}, },
"nags.settings.license.title": { "nags.settings.links.title": {
"defaultMessage": "Visit license settings" "defaultMessage": "Visit links settings"
}, },
"nags.settings.links.title": { "nags.settings.permissions.title": {
"defaultMessage": "Visit links settings" "defaultMessage": "Visit permissions dashboard"
}, },
"nags.settings.permissions.title": { "nags.settings.tags.title": {
"defaultMessage": "Visit permissions dashboard" "defaultMessage": "Visit tag settings"
}, },
"nags.settings.tags.title": { "nags.settings.title": {
"defaultMessage": "Visit tag settings" "defaultMessage": "Visit general settings"
}, },
"nags.settings.title": { "nags.settings.versions.title": {
"defaultMessage": "Visit general settings" "defaultMessage": "Visit versions settings"
}, },
"nags.settings.versions.title": { "nags.summary-special-formatting.description": {
"defaultMessage": "Visit versions settings" "defaultMessage": "Your summary should not contain formatting, line breaks, or special characters, since the summary will only display plain text."
}, },
"nags.summary-special-formatting.description": { "nags.summary-special-formatting.title": {
"defaultMessage": "Your summary should not contain formatting, line breaks, or special characters, since the summary will only display plain text." "defaultMessage": "Clean up the summary"
}, },
"nags.summary-special-formatting.title": { "nags.summary-too-short.title": {
"defaultMessage": "Clean up the summary" "defaultMessage": "Expand the summary"
}, },
"nags.summary-too-short.title": { "nags.too-many-languages.description": {
"defaultMessage": "Expand the summary" "defaultMessage": "You've selected {languageCount, plural, one {# language} other {# languages}}. Please list only the languages your server actively supports."
}, },
"nags.too-many-languages.description": { "nags.too-many-languages.title": {
"defaultMessage": "You've selected {languageCount, plural, one {# language} other {# languages}}. Please list only the languages your server actively supports." "defaultMessage": "Select accurate languages"
}, },
"nags.too-many-languages.title": { "nags.too-many-tags-server.description": {
"defaultMessage": "Select accurate languages" "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.description": { "nags.too-many-tags-server.title": {
"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." "defaultMessage": "Select accurate tags"
}, },
"nags.too-many-tags-server.title": { "nags.too-many-tags.description": {
"defaultMessage": "Select accurate tags" "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.description": { "nags.too-many-tags.title": {
"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." "defaultMessage": "Select accurate tags"
}, },
"nags.too-many-tags.title": { "nags.upload-gallery-image.description": {
"defaultMessage": "Select accurate tags" "defaultMessage": "At least one gallery image is required to showcase the content of your {type}."
}, },
"nags.upload-gallery-image.description": { "nags.upload-gallery-image.description-resourcepack": {
"defaultMessage": "At least one gallery image is required to showcase the content of your {type}." "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-resourcepack": { "nags.upload-gallery-image.description-shader": {
"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." "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.description-shader": { "nags.upload-gallery-image.title": {
"defaultMessage": "At least three gallery images are required to showcase the content of your shader in a variety of situations and conditions." "defaultMessage": "Upload a gallery image"
}, },
"nags.upload-gallery-image.title": { "nags.upload-version.description": {
"defaultMessage": "Upload a gallery image" "defaultMessage": "At least one version is required for a project to be submitted for review."
}, },
"nags.upload-version.description": { "nags.upload-version.title": {
"defaultMessage": "At least one version is required for a project to be submitted for review." "defaultMessage": "Upload a version"
}, },
"nags.upload-version.title": { "nags.verify-external-links.description": {
"defaultMessage": "Upload a version" "defaultMessage": "Some of your external links may be using domains that are inappropriate for that type of link."
}, },
"nags.verify-external-links.description": { "nags.verify-external-links.title": {
"defaultMessage": "Some of your external links may be using domains that are inappropriate for that type of link." "defaultMessage": "Verify external links"
}, },
"nags.verify-external-links.title": { "nags.visit-links-settings.title": {
"defaultMessage": "Verify external links" "defaultMessage": "Visit links settings"
}, },
"nags.visit-links-settings.title": { "project.text-validation.description-profanity": {
"defaultMessage": "Visit links settings" "defaultMessage": "Excessive profanity is not allowed. Detected: {values}."
}, },
"project.text-validation.non-standard-text": { "project.text-validation.non-standard-text": {
"defaultMessage": "Non-standard text characters are not allowed." "defaultMessage": "Non-standard text characters are not allowed."
}, },
"project.text-validation.profanity": { "project.text-validation.profanity": {
"defaultMessage": "The detected profanity “{value}” is not allowed." "defaultMessage": "The detected profanity “{value}” is not allowed."
}, },
"project.text-validation.slur": { "project.text-validation.slur": {
"defaultMessage": "The detected slur “{value}” is not allowed." "defaultMessage": "The detected slur “{value}” is not allowed."
}, },
"project.text-validation.summary-link": { "project.text-validation.summary-link": {
"defaultMessage": "Links should not be included in project summaries." "defaultMessage": "Links should not be included in project summaries."
}, },
"project.text-validation.summary-matches-title": { "project.text-validation.summary-matches-title": {
"defaultMessage": "A project summary should not be the same as its title." "defaultMessage": "A project summary should not be the same as its title."
}, },
"project.text-validation.summary-too-short": { "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." "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": { "project.text-validation.title-game-version": {
"defaultMessage": "Project titles should not include the Minecraft version “{value}”." "defaultMessage": "Project titles should not include the Minecraft version “{value}”."
}, },
"project.text-validation.title-loader": { "project.text-validation.title-loader": {
"defaultMessage": "Project titles should not include the loader “{value}”." "defaultMessage": "Project titles should not include the loader “{value}”."
} }
} }
@@ -248,6 +248,44 @@ function isWordCharacter(character: string | undefined): boolean {
return character !== undefined && /^[\p{L}\p{M}\p{N}_]$/u.test(character) 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 { function getCharacterBefore(text: string, index: number): string | undefined {
if (index <= 0) return undefined if (index <= 0) return undefined
@@ -282,27 +320,45 @@ export function createProfanityValidator(
return { kind: pattern.kind, term } return { kind: pattern.kind, term }
}) })
const matcher = new RegExpMatcher({ const blacklistedTerms = entries.map(({ term }, id) => ({ id, pattern: parseRawPattern(term) }))
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: [ blacklistMatcherTransformers: [
resolveConfusablesTransformer(), ...baseTransformers,
resolveLeetSpeakTransformer(),
toAsciiLowerCaseTransformer(),
skipNonAlphabeticTransformer(), skipNonAlphabeticTransformer(),
collapseDuplicatesTransformer({ duplicateTransformer(),
customThresholds: getDuplicateThresholds(entries.map(({ term }) => term)),
}),
], ],
}) })
function findAll(text: string): ProfanityMatch[] { function findAll(text: string): ProfanityMatch[] {
const matches: 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 profanityPattern = entries[match.termId]
const end = match.endIndex + 1 const end = match.endIndex + 1
const rawText = text.slice(match.startIndex, end)
const matchKey = `${match.termId}:${match.startIndex}:${match.endIndex}`
if ( if (
!profanityPattern || !profanityPattern ||
(!strictMatches.has(matchKey) && !isCharacterByCharacterObfuscation(rawText)) ||
!isWholeWordMatch(text, match.startIndex, end) || !isWholeWordMatch(text, match.startIndex, end) ||
match.startIndex < (matches.at(-1)?.end ?? 0) match.startIndex < (matches.at(-1)?.end ?? 0)
) { ) {
@@ -311,7 +367,7 @@ export function createProfanityValidator(
matches.push({ matches.push({
...profanityPattern, ...profanityPattern,
rawText: text.slice(match.startIndex, end), rawText,
start: match.startIndex, start: match.startIndex,
end, end,
}) })
@@ -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) 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', () => { test('rejects any uncensored configured profanity', () => {
assert.equal(validateProfanity('A clean project').valid, true) assert.equal(validateProfanity('A clean project').valid, true)
assert.equal(validateProfanity('This is shit').valid, false) assert.equal(validateProfanity('This is shit').valid, false)
@@ -60,6 +60,10 @@ const messages = defineMessages({
id: 'project.text-validation.profanity', id: 'project.text-validation.profanity',
defaultMessage: 'The detected profanity “{value}” is not allowed.', defaultMessage: 'The detected profanity “{value}” is not allowed.',
}, },
descriptionProfanity: {
id: 'project.text-validation.description-profanity',
defaultMessage: 'Excessive profanity is not allowed. Detected: {values}',
},
nonStandardText: { nonStandardText: {
id: 'project.text-validation.non-standard-text', id: 'project.text-validation.non-standard-text',
defaultMessage: 'Non-standard text characters are not allowed.', defaultMessage: 'Non-standard text characters are not allowed.',
@@ -398,6 +402,20 @@ export function validateProjectDescription(
maxProfanityCount: DESCRIPTION_MAX_PROFANITY_COUNT, maxProfanityCount: DESCRIPTION_MAX_PROFANITY_COUNT,
nonStandardTextFailureThreshold: DESCRIPTION_NON_STANDARD_TEXT_FAILURE_THRESHOLD, 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 if (results.length > 0) return results
const normalizedDescription = normalizeProjectFieldText(description ?? '') const normalizedDescription = normalizeProjectFieldText(description ?? '')
@@ -158,10 +158,13 @@ test('allows one profanity match in descriptions but rejects a second match or a
code: 'text-profanity', code: 'text-profanity',
severity: 'error', severity: 'error',
message: { message: {
id: 'project.text-validation.profanity', id: 'project.text-validation.description-profanity',
defaultMessage: 'The detected profanity “{value}” is not allowed.', 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} nigga`)[0]?.code, 'text-slur')
assert.equal(validateProjectDescription(`${description} nigger`)[0]?.code, 'text-slur') assert.equal(validateProjectDescription(`${description} nigger`)[0]?.code, 'text-slur')