@jjlmoya/utils-creative 1.26.0 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/category/i18n/de.ts +2 -2
- package/src/category/i18n/en.ts +2 -2
- package/src/category/i18n/es.ts +2 -2
- package/src/category/i18n/fr.ts +2 -2
- package/src/category/i18n/id.ts +2 -2
- package/src/category/i18n/it.ts +2 -2
- package/src/category/i18n/ja.ts +2 -2
- package/src/category/i18n/ko.ts +2 -2
- package/src/category/i18n/nl.ts +2 -2
- package/src/category/i18n/pl.ts +2 -2
- package/src/category/i18n/pt.ts +2 -2
- package/src/category/i18n/ru.ts +2 -2
- package/src/category/i18n/sv.ts +2 -2
- package/src/category/i18n/tr.ts +2 -2
- package/src/category/i18n/zh.ts +2 -2
- package/src/category/seo.astro +1 -1
- package/src/components/PreviewNavSidebar.astro +1 -1
- package/src/components/PreviewToolbar.astro +1 -1
- package/src/env.d.ts +1 -1
- package/src/pages/index.astro +1 -1
- package/src/tests/bibliography_wellformed_export.test.ts +46 -0
- package/src/tests/category_seo_quality.test.ts +74 -0
- package/src/tests/faq_count.test.ts +1 -1
- package/src/tests/mocks/astro_mock.js +1 -1
- package/src/tests/no_h1_in_components.test.ts +1 -1
- package/src/tests/seo_parity.test.ts +2 -2
- package/src/tests/seo_translation_completeness.test.ts +75 -0
- package/src/tests/seo_wellformed_export.test.ts +65 -0
- package/src/tests/tool_validation.test.ts +17 -17
- package/src/tool/bead-pattern-generator/bibliography.astro +8 -8
- package/src/tool/bead-pattern-generator/i18n/de.ts +111 -111
- package/src/tool/bead-pattern-generator/i18n/en.ts +111 -111
- package/src/tool/bead-pattern-generator/i18n/es.ts +104 -104
- package/src/tool/bead-pattern-generator/i18n/fr.ts +111 -111
- package/src/tool/bead-pattern-generator/i18n/id.ts +111 -111
- package/src/tool/bead-pattern-generator/i18n/it.ts +111 -111
- package/src/tool/bead-pattern-generator/i18n/ja.ts +111 -111
- package/src/tool/bead-pattern-generator/i18n/ko.ts +111 -111
- package/src/tool/bead-pattern-generator/i18n/nl.ts +111 -111
- package/src/tool/bead-pattern-generator/i18n/pl.ts +111 -111
- package/src/tool/bead-pattern-generator/i18n/pt.ts +111 -111
- package/src/tool/bead-pattern-generator/i18n/ru.ts +111 -111
- package/src/tool/bead-pattern-generator/i18n/sv.ts +111 -111
- package/src/tool/bead-pattern-generator/i18n/tr.ts +111 -111
- package/src/tool/bead-pattern-generator/i18n/zh.ts +111 -111
- package/src/tool/bead-pattern-generator/seo.astro +19 -19
- package/src/tool/dice-roller/bibliography.astro +8 -8
- package/src/tool/dice-roller/i18n/de.ts +134 -134
- package/src/tool/dice-roller/i18n/en.ts +134 -134
- package/src/tool/dice-roller/i18n/es.ts +125 -125
- package/src/tool/dice-roller/i18n/fr.ts +134 -134
- package/src/tool/dice-roller/i18n/id.ts +134 -134
- package/src/tool/dice-roller/i18n/it.ts +134 -134
- package/src/tool/dice-roller/i18n/ja.ts +134 -134
- package/src/tool/dice-roller/i18n/ko.ts +134 -134
- package/src/tool/dice-roller/i18n/nl.ts +134 -134
- package/src/tool/dice-roller/i18n/pl.ts +134 -134
- package/src/tool/dice-roller/i18n/pt.ts +134 -134
- package/src/tool/dice-roller/i18n/ru.ts +134 -134
- package/src/tool/dice-roller/i18n/sv.ts +134 -134
- package/src/tool/dice-roller/i18n/tr.ts +134 -134
- package/src/tool/dice-roller/i18n/zh.ts +134 -134
- package/src/tool/dice-roller/seo.astro +15 -15
- package/src/tool/excuse-generator/bibliography.astro +8 -8
- package/src/tool/excuse-generator/i18n/de.ts +120 -120
- package/src/tool/excuse-generator/i18n/en.ts +120 -120
- package/src/tool/excuse-generator/i18n/es.ts +118 -118
- package/src/tool/excuse-generator/i18n/fr.ts +120 -120
- package/src/tool/excuse-generator/i18n/id.ts +120 -120
- package/src/tool/excuse-generator/i18n/it.ts +120 -120
- package/src/tool/excuse-generator/i18n/ja.ts +120 -120
- package/src/tool/excuse-generator/i18n/ko.ts +120 -120
- package/src/tool/excuse-generator/i18n/nl.ts +120 -120
- package/src/tool/excuse-generator/i18n/pl.ts +120 -120
- package/src/tool/excuse-generator/i18n/pt.ts +120 -120
- package/src/tool/excuse-generator/i18n/ru.ts +120 -120
- package/src/tool/excuse-generator/i18n/sv.ts +120 -120
- package/src/tool/excuse-generator/i18n/tr.ts +120 -120
- package/src/tool/excuse-generator/i18n/zh.ts +120 -120
- package/src/tool/excuse-generator/seo.astro +15 -15
- package/src/tool/fortune-cookie/bibliography.astro +8 -8
- package/src/tool/fortune-cookie/i18n/de.ts +124 -124
- package/src/tool/fortune-cookie/i18n/en.ts +124 -124
- package/src/tool/fortune-cookie/i18n/es.ts +125 -125
- package/src/tool/fortune-cookie/i18n/fr.ts +124 -124
- package/src/tool/fortune-cookie/i18n/id.ts +124 -124
- package/src/tool/fortune-cookie/i18n/it.ts +124 -124
- package/src/tool/fortune-cookie/i18n/ja.ts +124 -124
- package/src/tool/fortune-cookie/i18n/ko.ts +124 -124
- package/src/tool/fortune-cookie/i18n/nl.ts +124 -124
- package/src/tool/fortune-cookie/i18n/pl.ts +124 -124
- package/src/tool/fortune-cookie/i18n/pt.ts +124 -124
- package/src/tool/fortune-cookie/i18n/ru.ts +124 -124
- package/src/tool/fortune-cookie/i18n/sv.ts +124 -124
- package/src/tool/fortune-cookie/i18n/tr.ts +124 -124
- package/src/tool/fortune-cookie/i18n/zh.ts +124 -124
- package/src/tool/fortune-cookie/seo.astro +15 -15
- package/src/tool/synesthesia-painter/bibliography.astro +8 -8
- package/src/tool/synesthesia-painter/i18n/de.ts +118 -118
- package/src/tool/synesthesia-painter/i18n/en.ts +118 -118
- package/src/tool/synesthesia-painter/i18n/es.ts +110 -110
- package/src/tool/synesthesia-painter/i18n/fr.ts +118 -118
- package/src/tool/synesthesia-painter/i18n/id.ts +118 -118
- package/src/tool/synesthesia-painter/i18n/it.ts +118 -118
- package/src/tool/synesthesia-painter/i18n/ja.ts +118 -118
- package/src/tool/synesthesia-painter/i18n/ko.ts +118 -118
- package/src/tool/synesthesia-painter/i18n/nl.ts +118 -118
- package/src/tool/synesthesia-painter/i18n/pl.ts +118 -118
- package/src/tool/synesthesia-painter/i18n/pt.ts +118 -118
- package/src/tool/synesthesia-painter/i18n/ru.ts +118 -118
- package/src/tool/synesthesia-painter/i18n/sv.ts +118 -118
- package/src/tool/synesthesia-painter/i18n/tr.ts +118 -118
- package/src/tool/synesthesia-painter/i18n/zh.ts +118 -118
- package/src/tool/synesthesia-painter/seo.astro +15 -15
- package/src/tool/zalgo-generator/bibliography.astro +8 -8
- package/src/tool/zalgo-generator/i18n/de.ts +109 -109
- package/src/tool/zalgo-generator/i18n/en.ts +109 -109
- package/src/tool/zalgo-generator/i18n/es.ts +109 -109
- package/src/tool/zalgo-generator/i18n/fr.ts +109 -109
- package/src/tool/zalgo-generator/i18n/id.ts +109 -109
- package/src/tool/zalgo-generator/i18n/it.ts +109 -109
- package/src/tool/zalgo-generator/i18n/ja.ts +109 -109
- package/src/tool/zalgo-generator/i18n/ko.ts +109 -109
- package/src/tool/zalgo-generator/i18n/nl.ts +109 -109
- package/src/tool/zalgo-generator/i18n/pl.ts +109 -109
- package/src/tool/zalgo-generator/i18n/pt.ts +109 -109
- package/src/tool/zalgo-generator/i18n/ru.ts +109 -109
- package/src/tool/zalgo-generator/i18n/sv.ts +109 -109
- package/src/tool/zalgo-generator/i18n/tr.ts +109 -109
- package/src/tool/zalgo-generator/i18n/zh.ts +109 -109
- package/src/tool/zalgo-generator/seo.astro +15 -15
- package/src/types.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jjlmoya/utils-creative",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"lint": "eslint src/ --max-warnings 0 && stylelint \"src/**/*.{css,astro}\"",
|
|
28
28
|
"check": "astro check",
|
|
29
29
|
"type-check": "astro check",
|
|
30
|
-
"test": "vitest run",
|
|
30
|
+
"test": "vitest run --reporter=verbose --testTimeout=30000",
|
|
31
31
|
"preversion": "npm run lint && npm run test && npm run build",
|
|
32
32
|
"postversion": "git push && git push --tags",
|
|
33
33
|
"patch": "npm version patch",
|
package/src/category/i18n/de.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const content = {
|
|
2
2
|
slug: 'kreativitaet-freizeit',
|
|
3
3
|
title: 'Kreativität und Freizeit',
|
|
4
|
-
description: 'Werkzeuge
|
|
4
|
+
description: 'Kreative Werkzeuge für Inspiration, Unterhaltung, schnelle Ideen und praktische Freizeitprojekte.',
|
|
5
5
|
seo: [
|
|
6
6
|
{ type: 'title', text: 'Tägliche kreative Hilfsmittel', level: 2 },
|
|
7
|
-
{ type: 'paragraph', html: 'Von Ausredengeneratoren bis hin zu Bastelmustern - unsere Sammlung kreativer Werkzeuge ist für Unterhaltung und praktischen Gebrauch konzipiert.' }
|
|
7
|
+
{ type: 'paragraph', html: 'Von Ausredengeneratoren bis hin zu Bastelmustern - unsere Sammlung kreativer Werkzeuge ist für Unterhaltung und praktischen Gebrauch konzipiert. Lassen Sie sich einen Ausgangspunkt geben oder passen Sie ein Muster an Ihr eigenes Projekt an. Die Ergebnisse sind Vorschläge zum Weiterentwickeln und Erstellen, keine starren Anweisungen.' }
|
|
8
8
|
]
|
|
9
9
|
};
|
package/src/category/i18n/en.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const content = {
|
|
2
2
|
slug: 'creativity-leisure',
|
|
3
3
|
title: 'Creativity and Leisure',
|
|
4
|
-
description: '
|
|
4
|
+
description: 'Creative tools for inspiration, entertainment, quick ideas and practical leisure projects.',
|
|
5
5
|
seo: [
|
|
6
6
|
{ type: 'title', text: 'Daily Creative Utilities', level: 2 },
|
|
7
|
-
{ type: 'paragraph', html: 'From excuse generators to craft patterns, our collection of creative tools is designed for entertainment and practical use.' }
|
|
7
|
+
{ type: 'paragraph', html: 'From excuse generators to craft patterns, our collection of creative tools is designed for entertainment and practical use. Explore a prompt, generate a starting point or adapt a pattern to your own project. The results are suggestions to develop, edit and make your own rather than fixed instructions.' }
|
|
8
8
|
]
|
|
9
9
|
};
|
package/src/category/i18n/es.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const content = {
|
|
2
2
|
slug: 'creatividad-ocio',
|
|
3
3
|
title: 'Creatividad y Ocio',
|
|
4
|
-
description: 'Herramientas
|
|
4
|
+
description: 'Herramientas creativas para inspirarte, entretenerte, generar ideas y disfrutar de proyectos prácticos.',
|
|
5
5
|
seo: [
|
|
6
6
|
{ type: 'title', text: 'Utilerías Creativas para el Día a Día', level: 2 },
|
|
7
|
-
{ type: 'paragraph', html: 'Desde generadores de excusas hasta patrones para manualidades, nuestra colección de herramientas creativas está pensada para el entretenimiento y la utilidad práctica.' }
|
|
7
|
+
{ type: 'paragraph', html: 'Desde generadores de excusas hasta patrones para manualidades, nuestra colección de herramientas creativas está pensada para el entretenimiento y la utilidad práctica. Genera un punto de partida, adapta una idea a tu proyecto y edita el resultado a tu manera. Son sugerencias para desarrollar y crear, no instrucciones rígidas que debas seguir al pie de la letra.' }
|
|
8
8
|
]
|
|
9
9
|
};
|
package/src/category/i18n/fr.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const content = {
|
|
2
2
|
slug: 'creativite-loisirs',
|
|
3
3
|
title: 'Créativité et Loisirs',
|
|
4
|
-
description:
|
|
4
|
+
description: "Des outils créatifs pour trouver l'inspiration, se divertir, générer des idées et réaliser des projets pratiques.",
|
|
5
5
|
seo: [
|
|
6
6
|
{ type: 'title', text: 'Outils Créatifs Quotidiens', level: 2 },
|
|
7
|
-
{ type: 'paragraph', html: 'Des générateurs d\'excuses aux modèles de loisirs créatifs, notre collection d\'outils créatifs est conçue pour le divertissement et l\'usage pratique.' }
|
|
7
|
+
{ type: 'paragraph', html: 'Des générateurs d\'excuses aux modèles de loisirs créatifs, notre collection d\'outils créatifs est conçue pour le divertissement et l\'usage pratique. Générez un point de départ, adaptez une idée à votre projet et modifiez le résultat selon vos besoins. Ce sont des suggestions à développer, pas des consignes rigides à suivre.' }
|
|
8
8
|
]
|
|
9
9
|
};
|
package/src/category/i18n/id.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const content = {
|
|
2
2
|
slug: 'kreativitas-hiburan',
|
|
3
3
|
title: 'Kreativitas dan Hiburan',
|
|
4
|
-
description: 'Alat
|
|
4
|
+
description: 'Alat kreatif untuk mencari inspirasi, hiburan, ide cepat, dan proyek waktu luang yang praktis.',
|
|
5
5
|
seo: [
|
|
6
6
|
{ type: 'title', text: 'Utilitas Kreatif Harian', level: 2 },
|
|
7
|
-
{ type: 'paragraph', html: 'Dari pembuat alasan hingga pola kerajinan, koleksi alat kreatif kami dirancang untuk hiburan dan penggunaan praktis.' }
|
|
7
|
+
{ type: 'paragraph', html: 'Dari pembuat alasan hingga pola kerajinan, koleksi alat kreatif kami dirancang untuk hiburan dan penggunaan praktis. Gunakan hasilnya sebagai titik awal, sesuaikan ide dengan proyek Anda, lalu edit sesuai kebutuhan. Hasil ini adalah saran untuk dikembangkan, bukan petunjuk kaku yang harus diikuti.' }
|
|
8
8
|
]
|
|
9
9
|
};
|
package/src/category/i18n/it.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const content = {
|
|
2
2
|
slug: 'creativita-tempo-libero',
|
|
3
3
|
title: 'Creatività e Tempo Libero',
|
|
4
|
-
description: 'Strumenti
|
|
4
|
+
description: 'Strumenti creativi per trovare ispirazione, divertirsi, generare idee e realizzare progetti pratici.',
|
|
5
5
|
seo: [
|
|
6
6
|
{ type: 'title', text: 'Utility Creative Quotidiane', level: 2 },
|
|
7
|
-
{ type: 'paragraph', html: 'Dai generatori di scuse ai modelli per il fai-da-te, la nostra collezione di strumenti creativi è pensata per l\'intrattenimento e l\'uso pratico.' }
|
|
7
|
+
{ type: 'paragraph', html: 'Dai generatori di scuse ai modelli per il fai-da-te, la nostra collezione di strumenti creativi è pensata per l\'intrattenimento e l\'uso pratico. Genera un punto di partenza, adatta un\'idea al tuo progetto e modifica il risultato come preferisci. Sono suggerimenti da sviluppare, non istruzioni rigide da seguire.' }
|
|
8
8
|
]
|
|
9
9
|
};
|
package/src/category/i18n/ja.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const content = {
|
|
2
2
|
slug: 'creativity-leisure',
|
|
3
3
|
title: 'クリエイティビティとレジャー',
|
|
4
|
-
description: '
|
|
4
|
+
description: 'ひらめきや遊び、アイデア作り、実用的な趣味のプロジェクトに役立つクリエイティブツール。',
|
|
5
5
|
seo: [
|
|
6
6
|
{ type: 'title', text: '毎日のクリエイティブ・ユーティリティ', level: 2 },
|
|
7
|
-
{ type: 'paragraph', html: '
|
|
7
|
+
{ type: 'paragraph', html: '言い訳ジェネレーターから手芸パターンまで、クリエイティブツールを集めました。結果をアイデアの出発点として使い、自分のプロジェクトに合わせて編集できます。表示される内容は発展させるための提案であり、そのまま従う固定的な指示ではありません。' }
|
|
8
8
|
]
|
|
9
9
|
};
|
package/src/category/i18n/ko.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const content = {
|
|
2
2
|
slug: 'creativity-leisure',
|
|
3
3
|
title: '창의성과 여가',
|
|
4
|
-
description: '
|
|
4
|
+
description: '영감과 재미, 아이디어 발상, 실용적인 취미 프로젝트를 돕는 창작 도구 모음입니다.',
|
|
5
5
|
seo: [
|
|
6
6
|
{ type: 'title', text: '일상의 창의적인 유틸리티', level: 2 },
|
|
7
|
-
{ type: 'paragraph', html: '변명 생성기부터 공예 패턴까지,
|
|
7
|
+
{ type: 'paragraph', html: '변명 생성기부터 공예 패턴까지, 창의적인 도구를 한곳에 모았습니다. 결과를 아이디어의 출발점으로 삼고 자신의 프로젝트에 맞게 수정해 보세요. 표시되는 내용은 발전시킬 수 있는 제안이며 그대로 따라야 하는 고정된 지침은 아닙니다.' }
|
|
8
8
|
]
|
|
9
9
|
};
|
package/src/category/i18n/nl.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const content = {
|
|
2
2
|
slug: 'creativiteit-vrije-tijd',
|
|
3
3
|
title: 'Creativiteit en Vrije Tijd',
|
|
4
|
-
description: '
|
|
4
|
+
description: 'Creatieve hulpmiddelen voor inspiratie, entertainment, snelle ideeën en praktische vrijetijdsprojecten.',
|
|
5
5
|
seo: [
|
|
6
6
|
{ type: 'title', text: 'Dagelijkse Creatieve Hulpmiddelen', level: 2 },
|
|
7
|
-
{ type: 'paragraph', html: 'Van smoesjesgeneratoren tot knutselpatronen, onze collectie creatieve hulpmiddelen is ontworpen voor entertainment en praktisch gebruik.' }
|
|
7
|
+
{ type: 'paragraph', html: 'Van smoesjesgeneratoren tot knutselpatronen, onze collectie creatieve hulpmiddelen is ontworpen voor entertainment en praktisch gebruik. Gebruik een resultaat als startpunt, pas een idee aan uw project aan en bewerk het naar wens. Het zijn suggesties om verder te ontwikkelen, geen vaste instructies die u moet volgen.' }
|
|
8
8
|
]
|
|
9
9
|
};
|
package/src/category/i18n/pl.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const content = {
|
|
2
2
|
slug: 'kreatywnosc-i-rozrywka',
|
|
3
3
|
title: 'Kreatywność i Rozrywka',
|
|
4
|
-
description: '
|
|
4
|
+
description: 'Kreatywne narzędzia do szukania inspiracji, rozrywki, szybkich pomysłów i praktycznych projektów.',
|
|
5
5
|
seo: [
|
|
6
6
|
{ type: 'title', text: 'Codzienne Narzędzia Kreatywne', level: 2 },
|
|
7
|
-
{ type: 'paragraph', html: 'Od generatorów wymówek po wzory do rękodzieła, nasza kolekcja narzędzi kreatywnych
|
|
7
|
+
{ type: 'paragraph', html: 'Od generatorów wymówek po wzory do rękodzieła, nasza kolekcja narzędzi kreatywnych służy rozrywce i praktycznym zastosowaniom. Potraktuj wynik jako punkt wyjścia, dopasuj pomysł do swojego projektu i edytuj go według potrzeb. To propozycje do rozwinięcia, a nie sztywne instrukcje.' }
|
|
8
8
|
]
|
|
9
9
|
};
|
package/src/category/i18n/pt.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const content = {
|
|
2
2
|
slug: 'criatividade-lazer',
|
|
3
3
|
title: 'Criatividade e Lazer',
|
|
4
|
-
description: 'Ferramentas
|
|
4
|
+
description: 'Ferramentas criativas para inspiração, entretenimento, ideias rápidas e projetos práticos de lazer.',
|
|
5
5
|
seo: [
|
|
6
6
|
{ type: 'title', text: 'Utilitários Criativos Diários', level: 2 },
|
|
7
|
-
{ type: 'paragraph', html: 'Desde geradores de desculpas a padrões de artesanato, a nossa coleção de ferramentas criativas foi desenhada para entretenimento e uso prático.' }
|
|
7
|
+
{ type: 'paragraph', html: 'Desde geradores de desculpas a padrões de artesanato, a nossa coleção de ferramentas criativas foi desenhada para entretenimento e uso prático. Use o resultado como ponto de partida, adapte a ideia ao seu projeto e edite-a como preferir. São sugestões para desenvolver, não instruções rígidas a seguir.' }
|
|
8
8
|
]
|
|
9
9
|
};
|
package/src/category/i18n/ru.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const content = {
|
|
2
2
|
slug: 'tvorchestvo-i-dosug',
|
|
3
3
|
title: 'Творчество и досуг',
|
|
4
|
-
description: '
|
|
4
|
+
description: 'Творческие инструменты для вдохновения, развлечения, быстрых идей и практических проектов.',
|
|
5
5
|
seo: [
|
|
6
6
|
{ type: 'title', text: 'Ежедневные творческие утилиты', level: 2 },
|
|
7
|
-
{ type: 'paragraph', html: 'От генераторов оправданий до шаблонов для
|
|
7
|
+
{ type: 'paragraph', html: 'От генераторов оправданий до шаблонов для рукоделия, эта коллекция творческих инструментов подходит для развлечения и практических задач. Используйте результат как отправную точку, адаптируйте идею под свой проект и отредактируйте её. Это предложения для развития, а не жёсткие инструкции.' }
|
|
8
8
|
]
|
|
9
9
|
};
|
package/src/category/i18n/sv.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const content = {
|
|
2
2
|
slug: 'kreativitet-fritid',
|
|
3
3
|
title: 'Kreativitet och Fritid',
|
|
4
|
-
description: '
|
|
4
|
+
description: 'Kreativa verktyg för inspiration, underhållning, snabba idéer och praktiska fritidsprojekt.',
|
|
5
5
|
seo: [
|
|
6
6
|
{ type: 'title', text: 'Dagliga Kreativa Verktyg', level: 2 },
|
|
7
|
-
{ type: 'paragraph', html: 'Från ursäktsgeneratorer till hantverksmönster
|
|
7
|
+
{ type: 'paragraph', html: 'Från ursäktsgeneratorer till hantverksmönster är vår samling kreativa verktyg gjord för underhållning och praktisk användning. Använd resultatet som en startpunkt, anpassa idén till ditt projekt och redigera den efter behov. Det är förslag att utveckla, inte fasta instruktioner.' }
|
|
8
8
|
]
|
|
9
9
|
};
|
package/src/category/i18n/tr.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const content = {
|
|
2
2
|
slug: 'yaraticilik-bos-zaman',
|
|
3
3
|
title: 'Yaratıcılık ve Boş Zaman',
|
|
4
|
-
description: 'İlham
|
|
4
|
+
description: 'İlham, eğlence, hızlı fikirler ve pratik boş zaman projeleri için yaratıcı araçlar.',
|
|
5
5
|
seo: [
|
|
6
6
|
{ type: 'title', text: 'Günlük Yaratıcı Araçlar', level: 2 },
|
|
7
|
-
{ type: 'paragraph', html: 'Mazeret üreteçlerinden el işi desenlerine kadar yaratıcı araç koleksiyonumuz eğlence ve pratik kullanım için tasarlanmıştır.' }
|
|
7
|
+
{ type: 'paragraph', html: 'Mazeret üreteçlerinden el işi desenlerine kadar yaratıcı araç koleksiyonumuz eğlence ve pratik kullanım için tasarlanmıştır. Sonuçları bir başlangıç noktası olarak kullanın, fikri projenize uyarlayın ve istediğiniz gibi düzenleyin. Bunlar geliştirilecek önerilerdir; aynen uygulanması gereken katı talimatlar değildir.' }
|
|
8
8
|
]
|
|
9
9
|
};
|
package/src/category/i18n/zh.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const content = {
|
|
2
2
|
slug: 'creativity-leisure',
|
|
3
3
|
title: '创意与休闲',
|
|
4
|
-
description: '
|
|
4
|
+
description: '用于激发灵感、娱乐、快速构思和实践休闲项目的创意工具。',
|
|
5
5
|
seo: [
|
|
6
6
|
{ type: 'title', text: '每日创意实用工具', level: 2 },
|
|
7
|
-
{ type: 'paragraph', html: '
|
|
7
|
+
{ type: 'paragraph', html: '从借口生成器到手工图样,这些创意工具适合娱乐和实际使用。您可以把结果作为起点,根据自己的项目调整并编辑。生成内容是可以继续发展的建议,不是必须原样执行的固定指令。选择合适的方向后,再加入自己的判断和风格。' }
|
|
8
8
|
]
|
|
9
9
|
};
|
package/src/category/seo.astro
CHANGED
package/src/env.d.ts
CHANGED
package/src/pages/index.astro
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { readdirSync, readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { join, relative } from 'path';
|
|
4
|
+
|
|
5
|
+
function findBibliographyAstroFiles(dir: string): string[] {
|
|
6
|
+
const files: string[] = [];
|
|
7
|
+
if (!existsSync(dir)) return files;
|
|
8
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
9
|
+
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
const fullPath = join(dir, entry.name);
|
|
12
|
+
if (entry.isDirectory()) {
|
|
13
|
+
files.push(...findBibliographyAstroFiles(fullPath));
|
|
14
|
+
} else if (entry.isFile() && entry.name === 'bibliography.astro') {
|
|
15
|
+
files.push(fullPath);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return files;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const toolDir = join(process.cwd(), 'src', 'tool');
|
|
23
|
+
const bibliographyFiles = findBibliographyAstroFiles(toolDir);
|
|
24
|
+
|
|
25
|
+
describe('Bibliography Component Wellformed Export', () => {
|
|
26
|
+
it('found tool bibliography.astro components', () => {
|
|
27
|
+
expect(bibliographyFiles.length).toBeGreaterThan(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
bibliographyFiles.forEach((file) => {
|
|
31
|
+
const relativePath = relative(process.cwd(), file);
|
|
32
|
+
|
|
33
|
+
it(`${relativePath} should use SharedBibliography from @jjlmoya/utils-shared`, () => {
|
|
34
|
+
const content = readFileSync(file, 'utf-8');
|
|
35
|
+
|
|
36
|
+
const usesSharedComponent =
|
|
37
|
+
content.includes('@jjlmoya/utils-shared') &&
|
|
38
|
+
(content.includes('Bibliography') || content.includes('SharedBibliography'));
|
|
39
|
+
|
|
40
|
+
expect(
|
|
41
|
+
usesSharedComponent,
|
|
42
|
+
`File "${relativePath}" does not use the standard Bibliography component from @jjlmoya/utils-shared, resulting in unstyled or raw bibliography output.`,
|
|
43
|
+
).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { CategoryLocaleContent } from '../types';
|
|
3
|
+
|
|
4
|
+
const EXPECTED_LOCALES = [
|
|
5
|
+
'de', 'en', 'es', 'fr', 'id', 'it', 'ja', 'ko', 'nl', 'pl', 'pt', 'ru', 'sv', 'tr', 'zh',
|
|
6
|
+
] as const;
|
|
7
|
+
const COMPACT_LOCALES = new Set(['ja', 'ko', 'zh']);
|
|
8
|
+
|
|
9
|
+
const localeModules = import.meta.glob('../category/i18n/*.ts', { eager: true }) as Record<
|
|
10
|
+
string,
|
|
11
|
+
{ content: CategoryLocaleContent }
|
|
12
|
+
>;
|
|
13
|
+
|
|
14
|
+
function textFrom(value: unknown): string {
|
|
15
|
+
if (typeof value === 'string') return value.replace(/<[^>]*>/g, ' ');
|
|
16
|
+
if (Array.isArray(value)) return value.map(textFrom).join(' ');
|
|
17
|
+
if (value && typeof value === 'object') return Object.values(value).map(textFrom).join(' ');
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const STRONG_CLAIM = /\b(?:garantiz\w*|guarante\w*|validat(?:ed)|valid(?:ated|ado|ada|ados|adas|ée|ées)|actualizad\w*)\b/iu;
|
|
22
|
+
|
|
23
|
+
function getContents(): Map<string, CategoryLocaleContent> {
|
|
24
|
+
return new Map(
|
|
25
|
+
Object.entries(localeModules).map(([file, module]) => [file.match(/\/([a-z]{2})\.ts$/u)?.[1] ?? file, module.content]),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function validateContent(locale: string, content: CategoryLocaleContent): string[] {
|
|
30
|
+
const failures: string[] = [];
|
|
31
|
+
const check = (condition: boolean, message: string) => { if (!condition) failures.push(`${locale}: ${message}`); };
|
|
32
|
+
const minimumTitleLength = COMPACT_LOCALES.has(locale) ? 4 : 12;
|
|
33
|
+
const minimumDescriptionLength = COMPACT_LOCALES.has(locale) ? 20 : 60;
|
|
34
|
+
check(content.title.trim().length >= minimumTitleLength && content.title.trim().length <= 70, 'title must use a useful 4/12–70 character length');
|
|
35
|
+
check(content.description.trim().length >= minimumDescriptionLength && content.description.trim().length <= 180, 'description must use a useful 20/60–180 character length');
|
|
36
|
+
check(content.seo.length >= 2, 'SEO needs at least two sections');
|
|
37
|
+
check(content.seo.some((section) => section.type === 'paragraph'), 'SEO needs visible explanatory copy');
|
|
38
|
+
|
|
39
|
+
const seoText = textFrom(content.seo).replace(/\s+/g, ' ').trim();
|
|
40
|
+
const minimumSeoLength = COMPACT_LOCALES.has(locale) ? 120 : 240;
|
|
41
|
+
check(seoText.length >= minimumSeoLength, 'SEO copy must contain the minimum visible copy for its script');
|
|
42
|
+
check(content.seo.filter((section) => section.type === 'title').length >= 1, 'SEO needs a section heading');
|
|
43
|
+
check(!/[�]/u.test(seoText), 'SEO contains replacement characters');
|
|
44
|
+
check(!/ {2,}/u.test(seoText), 'SEO contains duplicated whitespace');
|
|
45
|
+
check(!STRONG_CLAIM.test(seoText), 'SEO contains an unsupported strong claim');
|
|
46
|
+
|
|
47
|
+
check(content.slug.trim().length > 0 && /^[a-z0-9-]+$/u.test(content.slug), 'slug must be a non-empty URL-safe value');
|
|
48
|
+
return failures;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('Category SEO quality contract', () => {
|
|
52
|
+
it('has one content file for every configured locale', () => {
|
|
53
|
+
const contents = getContents();
|
|
54
|
+
expect([...contents.keys()].sort()).toEqual([...EXPECTED_LOCALES].sort());
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('keeps metadata, SEO structure and copy quality above the minimum in every locale', () => {
|
|
58
|
+
const contents = getContents();
|
|
59
|
+
const english = contents.get('en');
|
|
60
|
+
expect(english, 'English category content is required as the structural reference').toBeDefined();
|
|
61
|
+
|
|
62
|
+
const failures = EXPECTED_LOCALES.flatMap((locale) => {
|
|
63
|
+
const content = contents.get(locale);
|
|
64
|
+
return content ? validateContent(locale, content) : [];
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
for (const locale of EXPECTED_LOCALES) {
|
|
68
|
+
const content = contents.get(locale);
|
|
69
|
+
if (!content) failures.push(`${locale}: category content is missing`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
expect(failures, 'category SEO quality failures').toEqual([]);
|
|
73
|
+
}, 30000);
|
|
74
|
+
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export default function () { return null; }
|
|
2
|
-
|
|
2
|
+
|
|
@@ -27,7 +27,7 @@ async function verifyLocaleParity(
|
|
|
27
27
|
expect(
|
|
28
28
|
locSeoCount,
|
|
29
29
|
`Locale ${loc} SEO sections count (${locSeoCount}) must match EN (${expected.seo})`,
|
|
30
|
-
).
|
|
30
|
+
).toBeGreaterThanOrEqual(expected.seo);
|
|
31
31
|
expect(
|
|
32
32
|
locFaqCount,
|
|
33
33
|
`Locale ${loc} FAQ items count (${locFaqCount}) must match EN (${expected.faq})`,
|
|
@@ -41,7 +41,7 @@ async function verifyLocaleParity(
|
|
|
41
41
|
describe('SEO & i18n Structural Parity Suite', () => {
|
|
42
42
|
ALL_ENTRIES.forEach((entry) => {
|
|
43
43
|
describe(`Tool: ${entry.id}`, () => {
|
|
44
|
-
it('all 15 locales should
|
|
44
|
+
it('all 15 locales should provide at least the complete English SEO section set', async () => {
|
|
45
45
|
const enContent = await entry.i18n.en?.();
|
|
46
46
|
expect(enContent).toBeDefined();
|
|
47
47
|
const expected: ExpectedCounts = {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
|
|
4
|
+
const ASIAN_LOCALES = ['ja', 'ko', 'zh'];
|
|
5
|
+
|
|
6
|
+
function getSimpleSectionLength(section: any): number {
|
|
7
|
+
if (section.type === 'title') return section.text ? section.text.length : 0;
|
|
8
|
+
if (section.type === 'paragraph') return section.html ? section.html.replace(/<[^>]*>/g, '').length : 0;
|
|
9
|
+
if (section.type === 'list') return section.items ? section.items.join('').length : 0;
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getComplexSectionLength(section: any): number {
|
|
14
|
+
if (section.type === 'diagnostic' || section.type === 'tip') {
|
|
15
|
+
return (section.title ? section.title.length : 0) + (section.html ? section.html.replace(/<[^>]*>/g, '').length : 0);
|
|
16
|
+
}
|
|
17
|
+
if (section.type === 'table') {
|
|
18
|
+
return ((section.headers || []).join('').length) + ((section.rows || []).flat().join('').length);
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getSectionLength(section: any): number {
|
|
24
|
+
return getSimpleSectionLength(section) || getComplexSectionLength(section);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function calculateSeoTextLength(seoSections: any[]): number {
|
|
28
|
+
return seoSections.reduce((acc, section) => acc + getSectionLength(section), 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function checkLocaleSeoLength(toolId: string, locale: string, localeLen: number, enLen: number): string | null {
|
|
32
|
+
const isAsian = ASIAN_LOCALES.includes(locale);
|
|
33
|
+
const minLength = isAsian ? 240 : Math.max(600, Math.floor(enLen * 0.35));
|
|
34
|
+
const msgType = isAsian ? 'suspiciously short' : 'truncated/lazy';
|
|
35
|
+
|
|
36
|
+
if (localeLen >= minLength) return null;
|
|
37
|
+
return `[LAZY SEO TRANSLATION] Tool "${toolId}" locale "${locale}" SEO text is ${msgType} (${localeLen} chars vs EN ${enLen} chars, expected min ${minLength})`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function auditSingleLocale(toolId: string, locale: string, loader: any, enSeoLength: number): Promise<string | null> {
|
|
41
|
+
if (locale === 'en' || !loader) return null;
|
|
42
|
+
const content = await loader();
|
|
43
|
+
if (!content.seo || !Array.isArray(content.seo)) return null;
|
|
44
|
+
|
|
45
|
+
return checkLocaleSeoLength(toolId, locale, calculateSeoTextLength(content.seo), enSeoLength);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('SEO Translation Completeness & Laziness Audit', () => {
|
|
49
|
+
ALL_TOOLS.forEach(({ entry }) => {
|
|
50
|
+
describe(`Tool: ${entry.id}`, () => {
|
|
51
|
+
it('should not have lazy or truncated SEO content compared to English reference', async () => {
|
|
52
|
+
const enLoader = entry.i18n['en' as keyof typeof entry.i18n];
|
|
53
|
+
if (!enLoader) return;
|
|
54
|
+
|
|
55
|
+
const enContent = await enLoader();
|
|
56
|
+
if (!enContent.seo || !Array.isArray(enContent.seo)) return;
|
|
57
|
+
|
|
58
|
+
const enSeoLength = calculateSeoTextLength(enContent.seo);
|
|
59
|
+
const failures: string[] = [];
|
|
60
|
+
|
|
61
|
+
for (const [locale, loader] of Object.entries(entry.i18n)) {
|
|
62
|
+
const failure = await auditSingleLocale(entry.id, locale, loader, enSeoLength);
|
|
63
|
+
if (failure) failures.push(failure);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
expect(
|
|
67
|
+
failures,
|
|
68
|
+
failures.length > 0
|
|
69
|
+
? `SEO translation completeness failures for "${entry.id}":\n${failures.map((failure, index) => `${index + 1}. ${failure}`).join('\n')}`
|
|
70
|
+
: undefined,
|
|
71
|
+
).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { readdirSync, readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { join, relative } from 'path';
|
|
4
|
+
|
|
5
|
+
function findSeoAstroFiles(dir: string): string[] {
|
|
6
|
+
const files: string[] = [];
|
|
7
|
+
if (!existsSync(dir)) return files;
|
|
8
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
9
|
+
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
const fullPath = join(dir, entry.name);
|
|
12
|
+
if (entry.isDirectory()) {
|
|
13
|
+
files.push(...findSeoAstroFiles(fullPath));
|
|
14
|
+
} else if (entry.isFile() && entry.name === 'seo.astro') {
|
|
15
|
+
files.push(fullPath);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return files;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const toolDir = join(process.cwd(), 'src', 'tool');
|
|
23
|
+
const seoFiles = findSeoAstroFiles(toolDir);
|
|
24
|
+
|
|
25
|
+
describe('SEO Component Wellformed Export', () => {
|
|
26
|
+
it('found tool seo.astro components', () => {
|
|
27
|
+
expect(seoFiles.length).toBeGreaterThan(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
seoFiles.forEach((file) => {
|
|
31
|
+
const relativePath = relative(process.cwd(), file);
|
|
32
|
+
|
|
33
|
+
it(`${relativePath} should dynamically load SEO sections and use SEORenderer`, () => {
|
|
34
|
+
const content = readFileSync(file, 'utf-8');
|
|
35
|
+
|
|
36
|
+
const usesSeoRenderer =
|
|
37
|
+
content.includes('@jjlmoya/utils-shared') &&
|
|
38
|
+
(content.includes('SEORenderer') || content.includes('SEOArticle'));
|
|
39
|
+
|
|
40
|
+
const acquiresDynamicContent =
|
|
41
|
+
content.includes('.i18n') ||
|
|
42
|
+
content.includes('loader') ||
|
|
43
|
+
content.includes('await loader') ||
|
|
44
|
+
content.includes('.seo');
|
|
45
|
+
|
|
46
|
+
const isBrokenPattern =
|
|
47
|
+
/sections\s*=\s*\[\s*\]/.test(content) && !acquiresDynamicContent;
|
|
48
|
+
|
|
49
|
+
expect(
|
|
50
|
+
usesSeoRenderer,
|
|
51
|
+
`File "${relativePath}" does not import or use SEORenderer from @jjlmoya/utils-shared.`,
|
|
52
|
+
).toBe(true);
|
|
53
|
+
|
|
54
|
+
expect(
|
|
55
|
+
isBrokenPattern,
|
|
56
|
+
`File "${relativePath}" relies on a static sections=[] prop default without fetching content from entry.i18n, resulting in empty SEO text on consumers.`,
|
|
57
|
+
).toBe(false);
|
|
58
|
+
|
|
59
|
+
expect(
|
|
60
|
+
acquiresDynamicContent,
|
|
61
|
+
`File "${relativePath}" does not fetch dynamic i18n content for SEO rendering.`,
|
|
62
|
+
).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { ALL_TOOLS } from '../tools';
|
|
3
|
-
import { creativeCategory } from '../data';
|
|
4
|
-
|
|
5
|
-
describe('Tool Validation Suite', () => {
|
|
6
|
-
describe('Library Registration', () => {
|
|
7
|
-
it('should have 7 tools in ALL_TOOLS', () => {
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(7);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('creativeCategory should be defined', () => {
|
|
12
|
-
expect(creativeCategory).toBeDefined();
|
|
13
|
-
expect(creativeCategory.i18n).toBeDefined();
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
import { creativeCategory } from '../data';
|
|
4
|
+
|
|
5
|
+
describe('Tool Validation Suite', () => {
|
|
6
|
+
describe('Library Registration', () => {
|
|
7
|
+
it('should have 7 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(7);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('creativeCategory should be defined', () => {
|
|
12
|
+
expect(creativeCategory).toBeDefined();
|
|
13
|
+
expect(creativeCategory.i18n).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
---
|
|
2
|
-
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
-
import { bibliography } from './bibliography';
|
|
4
|
-
|
|
5
|
-
if (!bibliography || bibliography.length === 0) return null;
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
<SharedBibliography links={bibliography} />
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { bibliography } from './bibliography';
|
|
4
|
+
|
|
5
|
+
if (!bibliography || bibliography.length === 0) return null;
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<SharedBibliography links={bibliography} />
|