From a926cff5f4b3ae97caae712ac0be03e4db3c2aaa Mon Sep 17 00:00:00 2001 From: tdgao Date: Fri, 28 Aug 2026 13:22:10 -0600 Subject: [PATCH] feat: bump description too short to error, add spam validation for desc and summary, update summary and description too short copy --- .../moderation/src/locales/ar-SA/index.json | 6 - .../moderation/src/locales/cs-CZ/index.json | 6 - .../moderation/src/locales/da-DK/index.json | 6 - .../moderation/src/locales/de-CH/index.json | 6 - .../moderation/src/locales/de-DE/index.json | 6 - .../moderation/src/locales/en-US/index.json | 16 ++- .../moderation/src/locales/es-419/index.json | 6 - .../moderation/src/locales/es-ES/index.json | 6 - .../moderation/src/locales/fi-FI/index.json | 3 - .../moderation/src/locales/fil-PH/index.json | 6 - .../moderation/src/locales/fr-FR/index.json | 6 - .../moderation/src/locales/he-IL/index.json | 6 - .../moderation/src/locales/hu-HU/index.json | 6 - .../moderation/src/locales/id-ID/index.json | 6 - .../moderation/src/locales/it-IT/index.json | 6 - .../moderation/src/locales/ja-JP/index.json | 6 - .../moderation/src/locales/ko-KR/index.json | 6 - .../moderation/src/locales/ms-MY/index.json | 6 - .../moderation/src/locales/nl-NL/index.json | 6 - .../moderation/src/locales/no-NO/index.json | 6 - .../moderation/src/locales/pl-PL/index.json | 6 - .../moderation/src/locales/pt-BR/index.json | 6 - .../moderation/src/locales/pt-PT/index.json | 6 - .../moderation/src/locales/ro-RO/index.json | 6 - .../moderation/src/locales/ru-RU/index.json | 6 - .../moderation/src/locales/sr-CS/index.json | 6 - .../moderation/src/locales/sv-SE/index.json | 6 - .../moderation/src/locales/th-TH/index.json | 3 - .../moderation/src/locales/tr-TR/index.json | 6 - .../moderation/src/locales/uk-UA/index.json | 6 - .../moderation/src/locales/vi-VN/index.json | 6 - .../moderation/src/locales/zh-CN/index.json | 6 - .../moderation/src/locales/zh-TW/index.json | 6 - .../src/validation-rules/rules/description.ts | 75 ++++++++---- .../src/validation-rules/rules/summary.ts | 22 +++- .../moderation/src/validation-rules/tests.ts | 51 +++++++- .../moderation/src/validators/spam/index.ts | 113 ++++++++++++++++++ .../moderation/src/validators/spam/tests.ts | 36 ++++++ 38 files changed, 280 insertions(+), 219 deletions(-) create mode 100644 packages/moderation/src/validators/spam/index.ts create mode 100644 packages/moderation/src/validators/spam/tests.ts diff --git a/packages/moderation/src/locales/ar-SA/index.json b/packages/moderation/src/locales/ar-SA/index.json index 528f7704e7..6c099f406f 100644 --- a/packages/moderation/src/locales/ar-SA/index.json +++ b/packages/moderation/src/locales/ar-SA/index.json @@ -53,12 +53,6 @@ "nags.check-disclosures.title": { "defaultMessage": "تحقق من إفصاحات المحتوى" }, - "nags.description-too-short.description": { - "defaultMessage": "وصفك هو {length, plural, one {# readable character} other {# readable characters}}. يُوصى بأن يكون على الأقل {minChars, plural, one {# character}other{#characters}} لإنشاء وصف واضح وغني بالمعلومات." - }, - "nags.description-too-short.title": { - "defaultMessage": "قم بتوسيع الوصف" - }, "nags.edit-description.title": { "defaultMessage": "تعديل الوصف" }, diff --git a/packages/moderation/src/locales/cs-CZ/index.json b/packages/moderation/src/locales/cs-CZ/index.json index 802f26b754..0d11fe3dca 100644 --- a/packages/moderation/src/locales/cs-CZ/index.json +++ b/packages/moderation/src/locales/cs-CZ/index.json @@ -53,12 +53,6 @@ "nags.check-disclosures.title": { "defaultMessage": "Zkontrolovat zveřejnění obsahu" }, - "nags.description-too-short.description": { - "defaultMessage": "Váš popis má {length, plural, one {# čitelný znak} few {# čitelné znaky} other {# čitelných znaků}}. Pro vytvoření jasného a informativního popisu doporučujeme alespoň {minChars, plural, one {# znak} few {# znaky} other {# znaků}}." - }, - "nags.description-too-short.title": { - "defaultMessage": "Rozšiřte popis" - }, "nags.edit-description.title": { "defaultMessage": "Upravte popis" }, diff --git a/packages/moderation/src/locales/da-DK/index.json b/packages/moderation/src/locales/da-DK/index.json index 32b76cce2b..25f4c3e81a 100644 --- a/packages/moderation/src/locales/da-DK/index.json +++ b/packages/moderation/src/locales/da-DK/index.json @@ -35,12 +35,6 @@ "nags.all-tags-selected.title": { "defaultMessage": "Vælg præcise tags" }, - "nags.description-too-short.description": { - "defaultMessage": "Din beskrivelse er {length, plural, one {# læsligt bogstav} other {# læslige bogstaver}}. Som minimum er {minChars, plural, one {# bogstav} other {# bogstaver}} anbefalet for at lave en klar og informativ beskrivelse." - }, - "nags.description-too-short.title": { - "defaultMessage": "Forlæng beskrivelsen" - }, "nags.edit-description.title": { "defaultMessage": "Rediger beskrivelsen" }, diff --git a/packages/moderation/src/locales/de-CH/index.json b/packages/moderation/src/locales/de-CH/index.json index bf959d56db..7f48903a0b 100644 --- a/packages/moderation/src/locales/de-CH/index.json +++ b/packages/moderation/src/locales/de-CH/index.json @@ -53,12 +53,6 @@ "nags.check-disclosures.title": { "defaultMessage": "Angaben zum Inhalt prüfen" }, - "nags.description-too-short.description": { - "defaultMessage": "Deine Beschreibung ist {length, plural, one {# lesbares Zeichen} other {# lesbare Zeichen}} lang. Mindestens {minChars, plural, one {1 Zeichen ist} other {# Zeichen sind}} empfohlen, um eine klare und informative Beschreibung zu erstellen." - }, - "nags.description-too-short.title": { - "defaultMessage": "Beschreibung erweitern" - }, "nags.edit-description.title": { "defaultMessage": "Beschreibung bearbeiten" }, diff --git a/packages/moderation/src/locales/de-DE/index.json b/packages/moderation/src/locales/de-DE/index.json index fb251a8452..aa67132a4b 100644 --- a/packages/moderation/src/locales/de-DE/index.json +++ b/packages/moderation/src/locales/de-DE/index.json @@ -53,12 +53,6 @@ "nags.check-disclosures.title": { "defaultMessage": "Angaben zum Inhalt prüfen" }, - "nags.description-too-short.description": { - "defaultMessage": "Deine Beschreibung enthält {length, plural, one {# lesbares Zeichen} other {# lesbare Zeichen}}. Es wird empfohlen, mindestens {minChars, plural, one {# Zeichen} other {# Zeichen}} zu verwenden, um eine klare und informative Beschreibung zu erstellen." - }, - "nags.description-too-short.title": { - "defaultMessage": "Beschreibung erweitern" - }, "nags.edit-description.title": { "defaultMessage": "Beschreibung bearbeiten" }, diff --git a/packages/moderation/src/locales/en-US/index.json b/packages/moderation/src/locales/en-US/index.json index e6e054a804..50812e7341 100644 --- a/packages/moderation/src/locales/en-US/index.json +++ b/packages/moderation/src/locales/en-US/index.json @@ -60,7 +60,7 @@ "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." + "defaultMessage": "Your description is too brief. Add more to clearly describe the project's purpose and function." }, "nags.description-too-short.title": { "defaultMessage": "Expand the description" @@ -245,6 +245,12 @@ "nags.project-description-slur.description": { "defaultMessage": "Your project cannot contain any slurs. Detected: “{value}”." }, + "nags.project-description-spam.description": { + "defaultMessage": "Repeated characters, words, or phrases cannot be used to pad a project description." + }, + "nags.project-description-spam.title": { + "defaultMessage": "Remove spam from the description" + }, "nags.project-name-non-standard-text.description": { "defaultMessage": "Non-standard text characters, such as “₮ɆӾ₮”, are not allowed." }, @@ -269,6 +275,12 @@ "nags.project-summary-slur.description": { "defaultMessage": "Your project summary cannot contain any slurs. Detected: “{value}”." }, + "nags.project-summary-spam.description": { + "defaultMessage": "Repeated characters, words, or phrases cannot be used to pad a project summary." + }, + "nags.project-summary-spam.title": { + "defaultMessage": "Remove spam from the summary" + }, "nags.review-permissions.description": { "defaultMessage": "Make sure you have provided proof of your permission to distribute any external content in your Modpack." }, @@ -390,7 +402,7 @@ "defaultMessage": "A project summary cannot be the same as it's 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." + "defaultMessage": "Your summary is too short. Add a sentence or two which describes your project." }, "project.text-validation.title-version-number": { "defaultMessage": "Names are not allowed to include version numbers." diff --git a/packages/moderation/src/locales/es-419/index.json b/packages/moderation/src/locales/es-419/index.json index 8e29d695c8..587ccd7169 100644 --- a/packages/moderation/src/locales/es-419/index.json +++ b/packages/moderation/src/locales/es-419/index.json @@ -53,12 +53,6 @@ "nags.check-disclosures.title": { "defaultMessage": "Comprobar el contenido de divulgación" }, - "nags.description-too-short.description": { - "defaultMessage": "Tu descripción tiene {length, plural, one {# carácter legible} other {# caracteres legibles}}. Se recomienda al menos {minChars, plural, one {# carácter} other {# caracteres}} para crear una descripción clara e informativa." - }, - "nags.description-too-short.title": { - "defaultMessage": "Expandir la descripción" - }, "nags.edit-description.title": { "defaultMessage": "Editar descripción" }, diff --git a/packages/moderation/src/locales/es-ES/index.json b/packages/moderation/src/locales/es-ES/index.json index 0f6e64ddfa..e6444804d0 100644 --- a/packages/moderation/src/locales/es-ES/index.json +++ b/packages/moderation/src/locales/es-ES/index.json @@ -53,12 +53,6 @@ "nags.check-disclosures.title": { "defaultMessage": "Comprobar divulgaciones de contenido" }, - "nags.description-too-short.description": { - "defaultMessage": "Tu descripción tiene {length, plural, one {# carácter legible} other {# caracteres legibles}}. Se recomienda un mínimo de {minChars, plural, one {# carácter} other {# caracteres}} para crear una descripción clara e informativa." - }, - "nags.description-too-short.title": { - "defaultMessage": "Amplíe la descripción" - }, "nags.edit-description.title": { "defaultMessage": "Editar descripción" }, diff --git a/packages/moderation/src/locales/fi-FI/index.json b/packages/moderation/src/locales/fi-FI/index.json index a98f6b0f5f..c4ec9ab672 100644 --- a/packages/moderation/src/locales/fi-FI/index.json +++ b/packages/moderation/src/locales/fi-FI/index.json @@ -32,9 +32,6 @@ "nags.all-tags-selected.title": { "defaultMessage": "Valitse sopivat tagit" }, - "nags.description-too-short.title": { - "defaultMessage": "Laajenna kuvausta" - }, "nags.edit-description.title": { "defaultMessage": "Muokkaa kuvausta" }, diff --git a/packages/moderation/src/locales/fil-PH/index.json b/packages/moderation/src/locales/fil-PH/index.json index 0da4fee097..27de44b87f 100644 --- a/packages/moderation/src/locales/fil-PH/index.json +++ b/packages/moderation/src/locales/fil-PH/index.json @@ -35,12 +35,6 @@ "nags.all-tags-selected.title": { "defaultMessage": "Pumili ng mga pinaktumpak na tag" }, - "nags.description-too-short.description": { - "defaultMessage": "Ang iyong paglalarawan ay may {length, plural, one {# mababasang titik} other {# na mababasang titik}}. Hindi kumulang sa {minChars, plural, one {# titik} other {# na titik}} ang inimumungkahi sa paglikha ng maliwanag at makabuluhang paglalarawan." - }, - "nags.description-too-short.title": { - "defaultMessage": "Palawakin ang paglalarawan" - }, "nags.edit-description.title": { "defaultMessage": "Baguhin ang paglalarawan" }, diff --git a/packages/moderation/src/locales/fr-FR/index.json b/packages/moderation/src/locales/fr-FR/index.json index d8cbb6e425..42a59fa96d 100644 --- a/packages/moderation/src/locales/fr-FR/index.json +++ b/packages/moderation/src/locales/fr-FR/index.json @@ -53,12 +53,6 @@ "nags.check-disclosures.title": { "defaultMessage": "Regarder les avertissements sur le contenu" }, - "nags.description-too-short.description": { - "defaultMessage": "Votre description contient {length, plural, one {# caractère lisible} other {# caractères lisibles}}. Il est recommandé d’en avoir au moins {minChars, plural, one {# caractère} other {# caractères}} pour obtenir une description claire et informative." - }, - "nags.description-too-short.title": { - "defaultMessage": "Étendre la description" - }, "nags.edit-description.title": { "defaultMessage": "Modifier la description" }, diff --git a/packages/moderation/src/locales/he-IL/index.json b/packages/moderation/src/locales/he-IL/index.json index da16c8c590..ff013cdd45 100644 --- a/packages/moderation/src/locales/he-IL/index.json +++ b/packages/moderation/src/locales/he-IL/index.json @@ -47,12 +47,6 @@ "nags.all-tags-selected.title": { "defaultMessage": "בחר תגיות מדויקות" }, - "nags.description-too-short.description": { - "defaultMessage": "התיאור שלך הוא {length, plural, one {# דמות} other {# דמויות}}. מומלץ להשתמש לפחות ב-{minChars, plural, one {# דמות} other {# דמויות}} כדי ליצור תיאור ברור ואינפורמטיבי." - }, - "nags.description-too-short.title": { - "defaultMessage": "הרחב את התיאור" - }, "nags.edit-description.title": { "defaultMessage": "ערוך תיאור" }, diff --git a/packages/moderation/src/locales/hu-HU/index.json b/packages/moderation/src/locales/hu-HU/index.json index a871e3581b..4c83f7832c 100644 --- a/packages/moderation/src/locales/hu-HU/index.json +++ b/packages/moderation/src/locales/hu-HU/index.json @@ -53,12 +53,6 @@ "nags.check-disclosures.title": { "defaultMessage": "Ellenőrizd a tartalomnyilatkozatokat" }, - "nags.description-too-short.description": { - "defaultMessage": "A jelenlegi leírás {length} olvasható karakter hosszú. Egy világos és informatív leíráshoz legalább {minChars} karakter ajánlott." - }, - "nags.description-too-short.title": { - "defaultMessage": "Fejtsd ki a leírást" - }, "nags.edit-description.title": { "defaultMessage": "Leírás szerkesztése" }, diff --git a/packages/moderation/src/locales/id-ID/index.json b/packages/moderation/src/locales/id-ID/index.json index cf647cad08..75ab20af5a 100644 --- a/packages/moderation/src/locales/id-ID/index.json +++ b/packages/moderation/src/locales/id-ID/index.json @@ -47,12 +47,6 @@ "nags.all-tags-selected.title": { "defaultMessage": "Pilih tag yang tepat" }, - "nags.description-too-short.description": { - "defaultMessage": "Panjang keterangan Anda adalah {length, plural, other {# karakter yang dapat dibaca}}. Setidaknya {minChars, plural, other {# karakter}} disarankan untuk membuat keterangan yang jelas dan informatif." - }, - "nags.description-too-short.title": { - "defaultMessage": "Kembangkan keterangan" - }, "nags.edit-description.title": { "defaultMessage": "Sunting keterangan" }, diff --git a/packages/moderation/src/locales/it-IT/index.json b/packages/moderation/src/locales/it-IT/index.json index 277e097136..fea0ea2746 100644 --- a/packages/moderation/src/locales/it-IT/index.json +++ b/packages/moderation/src/locales/it-IT/index.json @@ -50,12 +50,6 @@ "nags.check-disclosures.title": { "defaultMessage": "Controlla le informative" }, - "nags.description-too-short.description": { - "defaultMessage": "La descrizione è di {length, plural, one {# carattere} other {# caratteri}}. Usane almeno {minChars} per tenerla chiara e informativa." - }, - "nags.description-too-short.title": { - "defaultMessage": "Espandi la descrizione" - }, "nags.edit-description.title": { "defaultMessage": "Modifica la descrizione" }, diff --git a/packages/moderation/src/locales/ja-JP/index.json b/packages/moderation/src/locales/ja-JP/index.json index a6dc110c39..87abf4168b 100644 --- a/packages/moderation/src/locales/ja-JP/index.json +++ b/packages/moderation/src/locales/ja-JP/index.json @@ -50,12 +50,6 @@ "nags.check-disclosures.title": { "defaultMessage": "コンテンツの開示を確認" }, - "nags.description-too-short.description": { - "defaultMessage": "説明文が{length, plural, other {#文字}}しかありません。より明確で分かりやすい説明文を作成するためには、少なくとも{minChars, plural, other {#文字}}以上であることが推奨されます。" - }, - "nags.description-too-short.title": { - "defaultMessage": "説明を展開する" - }, "nags.edit-description.title": { "defaultMessage": "説明を編集する" }, diff --git a/packages/moderation/src/locales/ko-KR/index.json b/packages/moderation/src/locales/ko-KR/index.json index df56e50dab..a9a98170ea 100644 --- a/packages/moderation/src/locales/ko-KR/index.json +++ b/packages/moderation/src/locales/ko-KR/index.json @@ -47,12 +47,6 @@ "nags.all-tags-selected.title": { "defaultMessage": "올바른 태그를 선택해 주세요" }, - "nags.description-too-short.description": { - "defaultMessage": "작성한 설명이 {length, plural, one {#자} other {#자}}입니다. 명확하고 유익한 설명을 작성하려면 최소 {minChars, plural, one {#자} other {#자}}가 권장됩니다." - }, - "nags.description-too-short.title": { - "defaultMessage": "설명을 덧붙여주세요" - }, "nags.edit-description.title": { "defaultMessage": "설명 수정하기" }, diff --git a/packages/moderation/src/locales/ms-MY/index.json b/packages/moderation/src/locales/ms-MY/index.json index cc4d5f1afe..0d38c824c7 100644 --- a/packages/moderation/src/locales/ms-MY/index.json +++ b/packages/moderation/src/locales/ms-MY/index.json @@ -35,12 +35,6 @@ "nags.all-tags-selected.title": { "defaultMessage": "Pilih tag yang tepat" }, - "nags.description-too-short.description": { - "defaultMessage": "Panjang keterangan anda ialah {length, plural, other {# aksara yang boleh dibaca}}. Sekurang-kurangnya {minChars, plural, other {# aksara}} disyorkan untuk membuat keterangan yang jelas dan bermaklumat." - }, - "nags.description-too-short.title": { - "defaultMessage": "Kembangkan keterangan" - }, "nags.edit-description.title": { "defaultMessage": "Sunting keterangan" }, diff --git a/packages/moderation/src/locales/nl-NL/index.json b/packages/moderation/src/locales/nl-NL/index.json index 6ff0e471a2..985e30c548 100644 --- a/packages/moderation/src/locales/nl-NL/index.json +++ b/packages/moderation/src/locales/nl-NL/index.json @@ -47,12 +47,6 @@ "nags.all-tags-selected.title": { "defaultMessage": "Selecteer relevante tags" }, - "nags.description-too-short.description": { - "defaultMessage": "Je beschrijving is {length, plural, one{# leesbare tekens}other {# leesbare tekens}}. Ten minste {minChars, plural, one {# teken}other {# tekens}} is aangeraden om een duidelijke en informatieve beschrijving te maken." - }, - "nags.description-too-short.title": { - "defaultMessage": "Verleng de beschrijving" - }, "nags.edit-description.title": { "defaultMessage": "Pas de beschrijving aan" }, diff --git a/packages/moderation/src/locales/no-NO/index.json b/packages/moderation/src/locales/no-NO/index.json index bbd485e843..31480ad171 100644 --- a/packages/moderation/src/locales/no-NO/index.json +++ b/packages/moderation/src/locales/no-NO/index.json @@ -41,12 +41,6 @@ "nags.all-tags-selected.title": { "defaultMessage": "Velg aktuelle tagger" }, - "nags.description-too-short.description": { - "defaultMessage": "Din beskrivelse inneholder {length, plural, one {# leselig tegn} other {# leselige tegn}}. Minst {minChars, plural, one {# tegn} other {# tegn}} er anbefalt for å lage en klar og informativ beskrivelse." - }, - "nags.description-too-short.title": { - "defaultMessage": "Utvid beskrivelsen" - }, "nags.edit-description.title": { "defaultMessage": "Rediger beskrivelse" }, diff --git a/packages/moderation/src/locales/pl-PL/index.json b/packages/moderation/src/locales/pl-PL/index.json index ca638c2dc4..77567f7507 100644 --- a/packages/moderation/src/locales/pl-PL/index.json +++ b/packages/moderation/src/locales/pl-PL/index.json @@ -47,12 +47,6 @@ "nags.all-tags-selected.title": { "defaultMessage": "Wybierz odpowiednie tagi" }, - "nags.description-too-short.description": { - "defaultMessage": "Twój opis składa się z {length, plural, one {# czytenego znaku} other {# czytelnych znaków}}. Zalecane jest użycie chociaż {minChars, plural, one {# znaku} other {# znaków}} by stworzyć przejrzysty i informatywny opis." - }, - "nags.description-too-short.title": { - "defaultMessage": "Rozwiń opis" - }, "nags.edit-description.title": { "defaultMessage": "Edytuj opis" }, diff --git a/packages/moderation/src/locales/pt-BR/index.json b/packages/moderation/src/locales/pt-BR/index.json index 3b9613a803..0a2cf05b12 100644 --- a/packages/moderation/src/locales/pt-BR/index.json +++ b/packages/moderation/src/locales/pt-BR/index.json @@ -53,12 +53,6 @@ "nags.check-disclosures.title": { "defaultMessage": "Verificar divulgações de conteúdo" }, - "nags.description-too-short.description": { - "defaultMessage": "Sua descrição possui {length, plural, one {apenas # caractere legível} other {apenas # caracteres legíveis}}. No mínimo, {minChars, plural, one {# caractere é recomendado} other {# characteres são recomendados}} para criar uma descrição concisa e informativa." - }, - "nags.description-too-short.title": { - "defaultMessage": "Expandir a descrição" - }, "nags.edit-description.title": { "defaultMessage": "Edite a descrição" }, diff --git a/packages/moderation/src/locales/pt-PT/index.json b/packages/moderation/src/locales/pt-PT/index.json index dd6f805ac8..b5272f7e8d 100644 --- a/packages/moderation/src/locales/pt-PT/index.json +++ b/packages/moderation/src/locales/pt-PT/index.json @@ -41,12 +41,6 @@ "nags.all-tags-selected.title": { "defaultMessage": "Seleciona etiquetas adequadas" }, - "nags.description-too-short.description": { - "defaultMessage": "A tua descrição tem {length, plural, one {# caractere legível} other {# caracteres legíveis}}. Pelo menos {minChars, plural, one {# caractere} other {# caracteres}} são recomendados para criar uma descrição clara e informativa." - }, - "nags.description-too-short.title": { - "defaultMessage": "Expande a descrição" - }, "nags.edit-description.title": { "defaultMessage": "Edita a descrição" }, diff --git a/packages/moderation/src/locales/ro-RO/index.json b/packages/moderation/src/locales/ro-RO/index.json index 5ca9cb0b8b..7f2b0b6826 100644 --- a/packages/moderation/src/locales/ro-RO/index.json +++ b/packages/moderation/src/locales/ro-RO/index.json @@ -35,12 +35,6 @@ "nags.all-tags-selected.title": { "defaultMessage": "Selectează etichete precise" }, - "nags.description-too-short.description": { - "defaultMessage": "Descrierea dumneavoastră este {length, plural, one {# caracter lizibil} other {# caractere lizibile}}. Măcar {minChars, plural, one {# caracter este} other {# caractere sunt}} recomandate pentru a creea o descriere clară și informativă." - }, - "nags.description-too-short.title": { - "defaultMessage": "Extinde descrierea" - }, "nags.edit-description.title": { "defaultMessage": "Editează descrierea" }, diff --git a/packages/moderation/src/locales/ru-RU/index.json b/packages/moderation/src/locales/ru-RU/index.json index b8417214c7..4929d3aac8 100644 --- a/packages/moderation/src/locales/ru-RU/index.json +++ b/packages/moderation/src/locales/ru-RU/index.json @@ -53,12 +53,6 @@ "nags.check-disclosures.title": { "defaultMessage": "Проверьте маркировку" }, - "nags.description-too-short.description": { - "defaultMessage": "Описание содержит {length, plural, one {# читаемый символ} few {# читаемых символа} other {# читаемых символов}}. Для ясного и информативного описания рекомендуется не менее {minChars, plural, one {# символа} other {# символов}}." - }, - "nags.description-too-short.title": { - "defaultMessage": "Расширьте описание" - }, "nags.edit-description.title": { "defaultMessage": "Изменить описание" }, diff --git a/packages/moderation/src/locales/sr-CS/index.json b/packages/moderation/src/locales/sr-CS/index.json index 00dec21ad5..c031c927f8 100644 --- a/packages/moderation/src/locales/sr-CS/index.json +++ b/packages/moderation/src/locales/sr-CS/index.json @@ -53,12 +53,6 @@ "nags.check-disclosures.title": { "defaultMessage": "Proverite obaveštenja o sadržaju" }, - "nags.description-too-short.description": { - "defaultMessage": "Tvoj opis je {length, plural, one {# čitljiv karakter} few {# čitljiva karaktera} other {# čitljivih karaktera}}. Barem {minChars, plural, one {# karakter} other {# karaktera}} je preporučeno kreiranje jasnog i informativnog opisa." - }, - "nags.description-too-short.title": { - "defaultMessage": "Proširi opis" - }, "nags.edit-description.title": { "defaultMessage": "Promeni opis" }, diff --git a/packages/moderation/src/locales/sv-SE/index.json b/packages/moderation/src/locales/sv-SE/index.json index b39031e3d2..060b57c634 100644 --- a/packages/moderation/src/locales/sv-SE/index.json +++ b/packages/moderation/src/locales/sv-SE/index.json @@ -53,12 +53,6 @@ "nags.check-disclosures.title": { "defaultMessage": "Läs innehållsinformation" }, - "nags.description-too-short.description": { - "defaultMessage": "Din beskrivning är {length, plural, one {# tecken} other {# tecken}} lång. Minst {minChars, plural, one {# tecken} other {# tecken}} rekommenderas för en klar och informativ beskrivning." - }, - "nags.description-too-short.title": { - "defaultMessage": "Utöka beskrivningen" - }, "nags.edit-description.title": { "defaultMessage": "Redigera beskrivning" }, diff --git a/packages/moderation/src/locales/th-TH/index.json b/packages/moderation/src/locales/th-TH/index.json index 4a766df5ce..63848cf8c7 100644 --- a/packages/moderation/src/locales/th-TH/index.json +++ b/packages/moderation/src/locales/th-TH/index.json @@ -14,9 +14,6 @@ "nags.add-links.title": { "defaultMessage": "เพิ่มลิงก์ภายนอก" }, - "nags.description-too-short.title": { - "defaultMessage": "เพิ่มคำอธิบาย" - }, "nags.edit-description.title": { "defaultMessage": "แก้ไขคำอธิบาย" }, diff --git a/packages/moderation/src/locales/tr-TR/index.json b/packages/moderation/src/locales/tr-TR/index.json index 90f9442231..a88b4ee263 100644 --- a/packages/moderation/src/locales/tr-TR/index.json +++ b/packages/moderation/src/locales/tr-TR/index.json @@ -53,12 +53,6 @@ "nags.check-disclosures.title": { "defaultMessage": "İçerikle ilgili uyarıları kontrol edin" }, - "nags.description-too-short.description": { - "defaultMessage": "Açıklamanız {length, plural, one {# okunabilir karakter} other {# okunabilir karakter}} uzunluğunda. Net ve bilgilendirici bir açıklama oluşturmak için en az {minChars, plural, one {# karakter} other {# karakter}} önerilir." - }, - "nags.description-too-short.title": { - "defaultMessage": "Açıklamayı genişlet" - }, "nags.edit-description.title": { "defaultMessage": "Açıklamayı düzenle" }, diff --git a/packages/moderation/src/locales/uk-UA/index.json b/packages/moderation/src/locales/uk-UA/index.json index c4e23d84f9..28a9a1498c 100644 --- a/packages/moderation/src/locales/uk-UA/index.json +++ b/packages/moderation/src/locales/uk-UA/index.json @@ -53,12 +53,6 @@ "nags.check-disclosures.title": { "defaultMessage": "Перевірте маркування вмісту" }, - "nags.description-too-short.description": { - "defaultMessage": "Ваш опис містить {length, plural, one {# видимий символ} few {# видимі символів} other {# видимих символів}}. Радимо щонайменше {minChars, plural, one {# символ} few {# символи} other {# символів}} для чіткого й змістовного опису." - }, - "nags.description-too-short.title": { - "defaultMessage": "Доповніть опис" - }, "nags.edit-description.title": { "defaultMessage": "Відредагуйте опис" }, diff --git a/packages/moderation/src/locales/vi-VN/index.json b/packages/moderation/src/locales/vi-VN/index.json index 93502bead0..042fb29fbc 100644 --- a/packages/moderation/src/locales/vi-VN/index.json +++ b/packages/moderation/src/locales/vi-VN/index.json @@ -47,12 +47,6 @@ "nags.all-tags-selected.title": { "defaultMessage": "Chọn các thẻ chính xác" }, - "nags.description-too-short.description": { - "defaultMessage": "Mô tả của bạn hiện có {length, plural, one {# ký tự có thể đọc} other {# ký tự có thể đọc}}. Khuyến nghị nên có ít nhất {minChars, plural, one {# ký tự} other {# ký tự}} để tạo ra một mô tả rõ ràng và đầy đủ thông tin." - }, - "nags.description-too-short.title": { - "defaultMessage": "Mở rộng mô tả" - }, "nags.edit-description.title": { "defaultMessage": "Chỉnh sửa mô tả" }, diff --git a/packages/moderation/src/locales/zh-CN/index.json b/packages/moderation/src/locales/zh-CN/index.json index 79ad3ee7f0..6df7d43608 100644 --- a/packages/moderation/src/locales/zh-CN/index.json +++ b/packages/moderation/src/locales/zh-CN/index.json @@ -53,12 +53,6 @@ "nags.check-disclosures.title": { "defaultMessage": "查看内容披露" }, - "nags.description-too-short.description": { - "defaultMessage": "你的描述包含 {length, plural, other {# 个可读字符}}。建议至少包含 {minChars, plural, other {# 个字符}},让描述详尽清晰。" - }, - "nags.description-too-short.title": { - "defaultMessage": "扩展描述" - }, "nags.edit-description.title": { "defaultMessage": "编辑描述" }, diff --git a/packages/moderation/src/locales/zh-TW/index.json b/packages/moderation/src/locales/zh-TW/index.json index b9e60abb92..f8a26f226a 100644 --- a/packages/moderation/src/locales/zh-TW/index.json +++ b/packages/moderation/src/locales/zh-TW/index.json @@ -53,12 +53,6 @@ "nags.check-disclosures.title": { "defaultMessage": "檢查內容揭露資訊" }, - "nags.description-too-short.description": { - "defaultMessage": "你的描述目前長度為 {length, plural, other {# 個可閱讀字元}}。建議至少輸入 {minChars, plural, other {# 個字元}},以建立清晰且資訊完整的描述。" - }, - "nags.description-too-short.title": { - "defaultMessage": "擴展描述" - }, "nags.edit-description.title": { "defaultMessage": "編輯描述" }, diff --git a/packages/moderation/src/validation-rules/rules/description.ts b/packages/moderation/src/validation-rules/rules/description.ts index ee390c5179..d0f4ee5c79 100644 --- a/packages/moderation/src/validation-rules/rules/description.ts +++ b/packages/moderation/src/validation-rules/rules/description.ts @@ -9,6 +9,7 @@ import { hostnameMatchesDomain, isIpAddress, } from '../../validators/links/syntax-checks.ts' +import { validateSpam } from '../../validators/spam/index.ts' import { evaluateRules } from '../evaluate-rules.ts' import { evaluateNonStandardText, @@ -33,6 +34,10 @@ const messages = defineMessages({ id: 'nags.description-too-short.title', defaultMessage: 'Expand the description', }, + removeSpam: { + id: 'nags.project-description-spam.title', + defaultMessage: 'Remove spam from the description', + }, shortenHeaders: { id: 'nags.long-headers.title', defaultMessage: 'Shorten headers', @@ -69,7 +74,12 @@ const messages = defineMessages({ tooShort: { id: '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.', + "Your description is too brief. Add more to clearly describe the project's purpose and function.", + }, + spam: { + id: 'nags.project-description-spam.description', + defaultMessage: + 'Repeated characters, words, or phrases cannot be used to pad a project description.', }, longHeaders: { id: 'nags.long-headers.description', @@ -85,7 +95,7 @@ const messages = defineMessages({ export const DESCRIPTION_MAX_PROFANITY_COUNT = 2 export const DESCRIPTION_NON_STANDARD_TEXT_FAILURE_THRESHOLD = 0.05 -export const MIN_DESCRIPTION_CHARS = 200 +export const MIN_DESCRIPTION_CHARS = 125 export const MAX_HEADER_LENGTH = 80 export const BANNED_DESCRIPTION_LINK_DOMAINS = [...URL_SHORTENERS] as const @@ -141,21 +151,29 @@ export function analyzeHeaderLength(markdown: string): { return { hasLongHeaders: longHeaders.length > 0, longHeaders } } -export function countText(markdown: string): number { - if (!markdown) return 0 +export function extractDescriptionText(markdown: string): string { + if (!markdown) return '' const withoutCode = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '') const withoutImagesAndLinks = withoutCode - .replace(/!\[[^\]]*]\([^)]+\)/g, ' ') + .replace(/!\[([^\]]*)]\([^)]+\)/g, '$1') .replace(/\[[^\]]*]\([^)]+\)/g, ' ') - const withoutHtml = withoutImagesAndLinks.replace(/<[^>]+>/g, ' ') + const withHtmlImageAltText = withoutImagesAndLinks.replace(/]*>/gi, (image) => { + const altMatch = image.match(/alt\s*=\s*(?:"([^"]*)"|'([^']*)')/i) + return altMatch?.[1] ?? altMatch?.[2] ?? ' ' + }) + const withoutHtml = withHtmlImageAltText.replace(/<[^>]+>/g, ' ') const withoutMarkdownSyntax = withoutHtml .replace(/^(?:>[ \t]?)+/gm, '') .replace(/^#{1,6}\s+/gm, ' ') .replace(/[*_~`>-]/g, ' ') .replace(/\|/g, ' ') - return withoutMarkdownSyntax.replace(/\s+/g, ' ').trim().length + return withoutMarkdownSyntax.replace(/\s+/g, ' ').trim() +} + +export function countText(markdown: string): number { + return extractDescriptionText(markdown).length } export function analyzeImageContent(markdown: string): { @@ -220,6 +238,32 @@ export const projectDescriptionValidationRules = { nag: { title: messages.addDescription, ...commonNagPresentation }, }, }, + 'description-too-short': { + severity: 'error', + evaluate: (description) => { + const normalized = normalizeProjectFieldText(description ?? '') + if (!normalized) return { valid: true } + + const length = countText(normalized) + return length >= MIN_DESCRIPTION_CHARS + ? { valid: true } + : { valid: false, values: { length, minChars: MIN_DESCRIPTION_CHARS } } + }, + presentation: { + message: messages.tooShort, + nag: { title: messages.expandDescription, ...commonNagPresentation }, + }, + }, + 'project-description-spam': { + severity: 'error', + evaluate: (description) => ({ + valid: validateSpam(extractDescriptionText(description ?? '')).valid, + }), + presentation: { + message: messages.spam, + nag: { title: messages.removeSpam, ...commonNagPresentation }, + }, + }, 'project-description-banned-link': { severity: 'error', evaluate: (description) => { @@ -235,23 +279,6 @@ export const projectDescriptionValidationRules = { nag: { title: messages.fixDescription, ...commonNagPresentation }, }, }, - 'description-too-short': { - severity: 'warning', - evaluate: (description) => { - const normalized = normalizeProjectFieldText(description ?? '') - if (!normalized) return { valid: true } - const length = countText(normalized) - if (length < MIN_DESCRIPTION_CHARS) { - return { valid: false, values: { length, minChars: MIN_DESCRIPTION_CHARS } } - } else { - return { valid: true } - } - }, - presentation: { - message: messages.tooShort, - nag: { title: messages.expandDescription, ...commonNagPresentation }, - }, - }, 'long-headers': { severity: 'warning', evaluate: (description) => { diff --git a/packages/moderation/src/validation-rules/rules/summary.ts b/packages/moderation/src/validation-rules/rules/summary.ts index 67bd505bb0..d7d30b095b 100644 --- a/packages/moderation/src/validation-rules/rules/summary.ts +++ b/packages/moderation/src/validation-rules/rules/summary.ts @@ -3,6 +3,7 @@ import LinkifyIt from 'linkify-it' import tlds from 'tlds' with { type: 'json' } import type { Nag, ProjectValidationContext } from '../../types/nags.ts' +import { validateSpam } from '../../validators/spam/index.ts' import { evaluateRules } from '../evaluate-rules.ts' import { evaluateNonStandardText, @@ -27,6 +28,10 @@ const messages = defineMessages({ id: 'nags.summary-too-short.title', defaultMessage: 'Expand the summary', }, + removeSpam: { + id: 'nags.project-summary-spam.title', + defaultMessage: 'Remove spam from the summary', + }, cleanUpSummary: { id: 'nags.summary-special-formatting.title', defaultMessage: 'Clean up the summary', @@ -54,7 +59,12 @@ const messages = defineMessages({ tooShort: { id: '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.', + 'Your summary is too short. Add a sentence or two which describes your project.', + }, + spam: { + id: 'nags.project-summary-spam.description', + defaultMessage: + 'Repeated characters, words, or phrases cannot be used to pad a project summary.', }, specialFormatting: { id: 'nags.summary-special-formatting.description', @@ -159,6 +169,16 @@ export const projectSummaryValidationRules = { nag: { title: messages.expandSummary, ...commonNagPresentation }, }, }, + 'project-summary-spam': { + severity: 'error', + evaluate: ({ summary }) => ({ + valid: validateSpam(normalizeProjectFieldText(summary ?? '')).valid, + }), + presentation: { + message: messages.spam, + nag: { title: messages.removeSpam, ...commonNagPresentation }, + }, + }, 'summary-special-formatting': { severity: 'error', evaluate: ({ summary }) => ({ diff --git a/packages/moderation/src/validation-rules/tests.ts b/packages/moderation/src/validation-rules/tests.ts index e5048382d0..ae209e1161 100644 --- a/packages/moderation/src/validation-rules/tests.ts +++ b/packages/moderation/src/validation-rules/tests.ts @@ -81,14 +81,14 @@ test('collects every matching project name rule', () => { ) assert.deepEqual(matches[0]?.values, { value: 'fuck' }) assert.equal(matches[1]?.rule.severity, 'error') - assert.equal(matches[2]?.rule.severity, 'warning') + assert.equal(matches[2]?.rule.severity, 'error') }) test('derives project name field presentation from the matching rule', () => { assert.deepEqual(validateProjectNameField('Minecraft'), [ { code: 'minecraft-title-clause', - severity: 'warning', + severity: 'error', message: { id: 'nags.minecraft-title-clause.description', defaultMessage: @@ -103,7 +103,7 @@ test('derives project name nags from the same matching rule', () => { const [nag] = toNags(evaluateRules('Minecraft', projectNameValidationRules)) assert.equal(nag?.id, 'minecraft-title-clause') - assert.equal(nag?.status, 'warning') + assert.equal(nag?.status, 'required') assert.equal(nag?.title.id, 'nags.minecraft-title-clause.title') assert.equal(nag?.link?.path, 'settings') assert.equal(nag?.link?.title.id, 'nags.edit-title.title') @@ -116,7 +116,7 @@ test('validates summary content from one rule set', () => { ({ code, severity }) => ({ code, severity }), ), [ - { code: 'summary-too-short', severity: 'warning' }, + { code: 'summary-too-short', severity: 'error' }, { code: 'summary-special-formatting', severity: 'error' }, ], ) @@ -129,6 +129,16 @@ test('validates summary content from one rule set', () => { ) }) +test('rejects repeated summary padding', () => { + assert.deepEqual( + validateProjectSummary({ + summary: 'Useful project! '.repeat(3), + name: 'Project title', + }).map(({ code }) => code), + ['project-summary-spam'], + ) +}) + test('rejects every link and IP address in project summaries', () => { for (const summary of [ 'Visit https://example.dev for more information about this project', @@ -178,7 +188,26 @@ test('validates description requirements and simultaneous recommendations', () = const description = `${'# '.concat('A'.repeat(81))}\n![](one.png)\n![](two.png)\n![](three.png)\n![](four.png)` assert.deepEqual( validateProjectDescription(description).map(({ code }) => code), - ['description-too-short', 'long-headers', 'missing-alt-text'], + ['description-too-short', 'project-description-spam', 'long-headers', 'missing-alt-text'], + ) +}) + +test('requires 125 readable description characters', () => { + const description = + 'This project adds useful tools, flexible behavior, accessible documentation, polished gameplay, and support for every player.' + + assert.equal(countText(description), MIN_DESCRIPTION_CHARS) + assert.deepEqual( + validateProjectDescription(description.slice(0, -1)).map(({ code }) => code), + ['description-too-short'], + ) + assert.deepEqual(validateProjectDescription(description), []) +}) + +test('rejects repeated description padding', () => { + assert.deepEqual( + validateProjectDescription('Useful project! '.repeat(10)).map(({ code }) => code), + ['project-description-spam'], ) }) @@ -190,10 +219,20 @@ test('requires alt text for description images', () => { assert.deepEqual(analyzeImageContent(''), { hasEmptyAltText: true }) }) +test('counts image alt text as readable description text', () => { + assert.equal(countText('![Project screenshot](screenshot.png)'), 'Project screenshot'.length) + assert.equal( + countText('Project screenshot'), + 'Project screenshot'.length, + ) +}) + test('counts blockquote content as readable description text', () => { assert.equal(countText('> Quoted text'), 'Quoted text'.length) - const quotedDescription = `> ${'A'.repeat(MIN_DESCRIPTION_CHARS)}` + const quotedDescription = + '> This project adds useful tools, flexible behavior, accessible documentation, polished gameplay, and support for every player.' + assert.equal(countText(quotedDescription), MIN_DESCRIPTION_CHARS) assert.equal( validateProjectDescription(quotedDescription).some( ({ code }) => code === 'description-too-short', diff --git a/packages/moderation/src/validators/spam/index.ts b/packages/moderation/src/validators/spam/index.ts new file mode 100644 index 0000000000..ba8e7e18e5 --- /dev/null +++ b/packages/moderation/src/validators/spam/index.ts @@ -0,0 +1,113 @@ +export type SpamMatchKind = 'character' | 'word' | 'phrase' + +export interface SpamMatch { + kind: SpamMatchKind + value: string +} + +export interface SpamResult { + valid: boolean + firstMatch?: SpamMatch +} + +export const MIN_REPEATED_CHARACTER_COUNT = 8 +export const MIN_REPEATED_WORD_COUNT = 4 +export const MIN_REPEATED_PHRASE_COUNT = 3 +export const MAX_REPEATED_PHRASE_WORDS = 8 + +const REPEATABLE_CHARACTER_PATTERN = /\S/u +const WORD_PATTERN = /[\p{L}\p{M}\p{N}]+/gu + +function findRepeatedCharacter(text: string): SpamMatch | undefined { + let previousCharacter: string | undefined + let repeatedCount = 0 + + for (const character of text.normalize('NFC')) { + const normalizedCharacter = character.toLowerCase() + if ( + REPEATABLE_CHARACTER_PATTERN.test(normalizedCharacter) && + normalizedCharacter === previousCharacter + ) { + repeatedCount++ + } else { + previousCharacter = normalizedCharacter + repeatedCount = REPEATABLE_CHARACTER_PATTERN.test(normalizedCharacter) ? 1 : 0 + } + + if (repeatedCount >= MIN_REPEATED_CHARACTER_COUNT) { + return { kind: 'character', value: character.repeat(repeatedCount) } + } + } + + return undefined +} + +function getWords(text: string): string[] { + return [...text.normalize('NFC').toLowerCase().matchAll(WORD_PATTERN)].map((match) => match[0]) +} + +function findRepeatedWord(words: readonly string[]): SpamMatch | undefined { + let repeatedCount = 1 + + for (let index = 1; index < words.length; index++) { + repeatedCount = words[index] === words[index - 1] ? repeatedCount + 1 : 1 + if (repeatedCount >= MIN_REPEATED_WORD_COUNT) { + return { kind: 'word', value: words[index] } + } + } + + return undefined +} + +function phrasesMatch( + words: readonly string[], + firstStart: number, + secondStart: number, + size: number, +) { + for (let offset = 0; offset < size; offset++) { + if (words[firstStart + offset] !== words[secondStart + offset]) return false + } + + return true +} + +function findRepeatedPhrase(words: readonly string[]): SpamMatch | undefined { + const maxPhraseWords = Math.min( + MAX_REPEATED_PHRASE_WORDS, + Math.floor(words.length / MIN_REPEATED_PHRASE_COUNT), + ) + + for (let phraseWords = maxPhraseWords; phraseWords >= 2; phraseWords--) { + const repeatedWords = phraseWords * MIN_REPEATED_PHRASE_COUNT + for (let start = 0; start + repeatedWords <= words.length; start++) { + let matches = true + for (let repetition = 1; repetition < MIN_REPEATED_PHRASE_COUNT; repetition++) { + if (!phrasesMatch(words, start, start + repetition * phraseWords, phraseWords)) { + matches = false + break + } + } + + if (matches) { + return { kind: 'phrase', value: words.slice(start, start + phraseWords).join(' ') } + } + } + } + + return undefined +} + +/** + The spam validator checks normalized, readable description text in this order: + Characters: the same non-whitespace character repeated 8 times consecutively, e.g. aaaaaaaa. + Words: the same word repeated 4 times consecutively, case-insensitively, e.g. Great great GREAT great. + Phrases: a 2–8 word phrase repeated 3 times consecutively. Punctuation and capitalization are ignored, so best project, best project! BEST PROJECT is rejected. + */ +export function validateSpam(text: string): SpamResult { + const words = getWords(text) + const firstMatch = + findRepeatedCharacter(text) ?? findRepeatedWord(words) ?? findRepeatedPhrase(words) + + return firstMatch ? { valid: false, firstMatch } : { valid: true } +} diff --git a/packages/moderation/src/validators/spam/tests.ts b/packages/moderation/src/validators/spam/tests.ts new file mode 100644 index 0000000000..a17bef315e --- /dev/null +++ b/packages/moderation/src/validators/spam/tests.ts @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { validateSpam } from './index.ts' + +test('allows ordinary prose', () => { + assert.equal( + validateSpam('This project adds a configurable compass for exploring large worlds.').valid, + true, + ) +}) + +test('detects repeated characters', () => { + assert.deepEqual(validateSpam('aaaaaaaa').firstMatch, { + kind: 'character', + value: 'aaaaaaaa', + }) +}) + +test('detects repeated words case-insensitively', () => { + assert.deepEqual(validateSpam('Great great GREAT great').firstMatch, { + kind: 'word', + value: 'great', + }) +}) + +test('detects repeated phrases across punctuation', () => { + assert.deepEqual(validateSpam('best project, best project! BEST PROJECT').firstMatch, { + kind: 'phrase', + value: 'best project', + }) +}) + +test('allows repetition below the spam thresholds', () => { + assert.equal(validateSpam('so so so good good phrase here phrase here').valid, true) +})