@jjlmoya/utils-creative 1.25.0 → 1.27.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.
Files changed (134) hide show
  1. package/package.json +2 -2
  2. package/src/category/i18n/de.ts +2 -2
  3. package/src/category/i18n/en.ts +2 -2
  4. package/src/category/i18n/es.ts +2 -2
  5. package/src/category/i18n/fr.ts +2 -2
  6. package/src/category/i18n/id.ts +2 -2
  7. package/src/category/i18n/it.ts +2 -2
  8. package/src/category/i18n/ja.ts +2 -2
  9. package/src/category/i18n/ko.ts +2 -2
  10. package/src/category/i18n/nl.ts +2 -2
  11. package/src/category/i18n/pl.ts +2 -2
  12. package/src/category/i18n/pt.ts +2 -2
  13. package/src/category/i18n/ru.ts +2 -2
  14. package/src/category/i18n/sv.ts +2 -2
  15. package/src/category/i18n/tr.ts +2 -2
  16. package/src/category/i18n/zh.ts +2 -2
  17. package/src/category/seo.astro +1 -1
  18. package/src/components/PreviewNavSidebar.astro +1 -1
  19. package/src/components/PreviewToolbar.astro +1 -1
  20. package/src/env.d.ts +1 -1
  21. package/src/pages/index.astro +1 -1
  22. package/src/tests/bibliography_wellformed_export.test.ts +46 -0
  23. package/src/tests/category_seo_quality.test.ts +74 -0
  24. package/src/tests/faq_count.test.ts +1 -1
  25. package/src/tests/mocks/astro_mock.js +1 -1
  26. package/src/tests/no_h1_in_components.test.ts +1 -1
  27. package/src/tests/seo_parity.test.ts +2 -2
  28. package/src/tests/seo_translation_completeness.test.ts +69 -0
  29. package/src/tests/seo_wellformed_export.test.ts +65 -0
  30. package/src/tests/spanish_leakage.test.ts +5 -1
  31. package/src/tests/tool_validation.test.ts +17 -17
  32. package/src/tool/bead-pattern-generator/bibliography.astro +8 -8
  33. package/src/tool/bead-pattern-generator/i18n/de.ts +111 -111
  34. package/src/tool/bead-pattern-generator/i18n/en.ts +111 -111
  35. package/src/tool/bead-pattern-generator/i18n/es.ts +104 -104
  36. package/src/tool/bead-pattern-generator/i18n/fr.ts +111 -111
  37. package/src/tool/bead-pattern-generator/i18n/id.ts +111 -111
  38. package/src/tool/bead-pattern-generator/i18n/it.ts +111 -111
  39. package/src/tool/bead-pattern-generator/i18n/ja.ts +111 -111
  40. package/src/tool/bead-pattern-generator/i18n/ko.ts +111 -111
  41. package/src/tool/bead-pattern-generator/i18n/nl.ts +111 -111
  42. package/src/tool/bead-pattern-generator/i18n/pl.ts +111 -111
  43. package/src/tool/bead-pattern-generator/i18n/pt.ts +111 -111
  44. package/src/tool/bead-pattern-generator/i18n/ru.ts +111 -111
  45. package/src/tool/bead-pattern-generator/i18n/sv.ts +111 -111
  46. package/src/tool/bead-pattern-generator/i18n/tr.ts +111 -111
  47. package/src/tool/bead-pattern-generator/i18n/zh.ts +111 -111
  48. package/src/tool/bead-pattern-generator/seo.astro +19 -19
  49. package/src/tool/dice-roller/bibliography.astro +8 -8
  50. package/src/tool/dice-roller/i18n/de.ts +134 -134
  51. package/src/tool/dice-roller/i18n/en.ts +134 -134
  52. package/src/tool/dice-roller/i18n/es.ts +125 -125
  53. package/src/tool/dice-roller/i18n/fr.ts +134 -134
  54. package/src/tool/dice-roller/i18n/id.ts +134 -134
  55. package/src/tool/dice-roller/i18n/it.ts +134 -134
  56. package/src/tool/dice-roller/i18n/ja.ts +134 -134
  57. package/src/tool/dice-roller/i18n/ko.ts +134 -134
  58. package/src/tool/dice-roller/i18n/nl.ts +134 -134
  59. package/src/tool/dice-roller/i18n/pl.ts +134 -134
  60. package/src/tool/dice-roller/i18n/pt.ts +134 -134
  61. package/src/tool/dice-roller/i18n/ru.ts +134 -134
  62. package/src/tool/dice-roller/i18n/sv.ts +134 -134
  63. package/src/tool/dice-roller/i18n/tr.ts +134 -134
  64. package/src/tool/dice-roller/i18n/zh.ts +134 -134
  65. package/src/tool/dice-roller/seo.astro +15 -15
  66. package/src/tool/excuse-generator/bibliography.astro +8 -8
  67. package/src/tool/excuse-generator/i18n/de.ts +120 -120
  68. package/src/tool/excuse-generator/i18n/en.ts +120 -120
  69. package/src/tool/excuse-generator/i18n/es.ts +118 -118
  70. package/src/tool/excuse-generator/i18n/fr.ts +120 -120
  71. package/src/tool/excuse-generator/i18n/id.ts +120 -120
  72. package/src/tool/excuse-generator/i18n/it.ts +120 -120
  73. package/src/tool/excuse-generator/i18n/ja.ts +120 -120
  74. package/src/tool/excuse-generator/i18n/ko.ts +120 -120
  75. package/src/tool/excuse-generator/i18n/nl.ts +120 -120
  76. package/src/tool/excuse-generator/i18n/pl.ts +120 -120
  77. package/src/tool/excuse-generator/i18n/pt.ts +120 -120
  78. package/src/tool/excuse-generator/i18n/ru.ts +120 -120
  79. package/src/tool/excuse-generator/i18n/sv.ts +120 -120
  80. package/src/tool/excuse-generator/i18n/tr.ts +120 -120
  81. package/src/tool/excuse-generator/i18n/zh.ts +120 -120
  82. package/src/tool/excuse-generator/seo.astro +15 -15
  83. package/src/tool/fortune-cookie/bibliography.astro +8 -8
  84. package/src/tool/fortune-cookie/i18n/de.ts +124 -124
  85. package/src/tool/fortune-cookie/i18n/en.ts +124 -124
  86. package/src/tool/fortune-cookie/i18n/es.ts +125 -125
  87. package/src/tool/fortune-cookie/i18n/fr.ts +124 -124
  88. package/src/tool/fortune-cookie/i18n/id.ts +124 -124
  89. package/src/tool/fortune-cookie/i18n/it.ts +124 -124
  90. package/src/tool/fortune-cookie/i18n/ja.ts +124 -124
  91. package/src/tool/fortune-cookie/i18n/ko.ts +124 -124
  92. package/src/tool/fortune-cookie/i18n/nl.ts +124 -124
  93. package/src/tool/fortune-cookie/i18n/pl.ts +124 -124
  94. package/src/tool/fortune-cookie/i18n/pt.ts +124 -124
  95. package/src/tool/fortune-cookie/i18n/ru.ts +124 -124
  96. package/src/tool/fortune-cookie/i18n/sv.ts +124 -124
  97. package/src/tool/fortune-cookie/i18n/tr.ts +124 -124
  98. package/src/tool/fortune-cookie/i18n/zh.ts +124 -124
  99. package/src/tool/fortune-cookie/seo.astro +15 -15
  100. package/src/tool/synesthesia-painter/bibliography.astro +8 -8
  101. package/src/tool/synesthesia-painter/i18n/de.ts +118 -118
  102. package/src/tool/synesthesia-painter/i18n/en.ts +118 -118
  103. package/src/tool/synesthesia-painter/i18n/es.ts +110 -110
  104. package/src/tool/synesthesia-painter/i18n/fr.ts +118 -118
  105. package/src/tool/synesthesia-painter/i18n/id.ts +118 -118
  106. package/src/tool/synesthesia-painter/i18n/it.ts +118 -118
  107. package/src/tool/synesthesia-painter/i18n/ja.ts +118 -118
  108. package/src/tool/synesthesia-painter/i18n/ko.ts +118 -118
  109. package/src/tool/synesthesia-painter/i18n/nl.ts +118 -118
  110. package/src/tool/synesthesia-painter/i18n/pl.ts +118 -118
  111. package/src/tool/synesthesia-painter/i18n/pt.ts +118 -118
  112. package/src/tool/synesthesia-painter/i18n/ru.ts +118 -118
  113. package/src/tool/synesthesia-painter/i18n/sv.ts +118 -118
  114. package/src/tool/synesthesia-painter/i18n/tr.ts +118 -118
  115. package/src/tool/synesthesia-painter/i18n/zh.ts +118 -118
  116. package/src/tool/synesthesia-painter/seo.astro +15 -15
  117. package/src/tool/zalgo-generator/bibliography.astro +8 -8
  118. package/src/tool/zalgo-generator/i18n/de.ts +109 -109
  119. package/src/tool/zalgo-generator/i18n/en.ts +109 -109
  120. package/src/tool/zalgo-generator/i18n/es.ts +109 -109
  121. package/src/tool/zalgo-generator/i18n/fr.ts +109 -109
  122. package/src/tool/zalgo-generator/i18n/id.ts +109 -109
  123. package/src/tool/zalgo-generator/i18n/it.ts +109 -109
  124. package/src/tool/zalgo-generator/i18n/ja.ts +109 -109
  125. package/src/tool/zalgo-generator/i18n/ko.ts +109 -109
  126. package/src/tool/zalgo-generator/i18n/nl.ts +109 -109
  127. package/src/tool/zalgo-generator/i18n/pl.ts +109 -109
  128. package/src/tool/zalgo-generator/i18n/pt.ts +109 -109
  129. package/src/tool/zalgo-generator/i18n/ru.ts +109 -109
  130. package/src/tool/zalgo-generator/i18n/sv.ts +109 -109
  131. package/src/tool/zalgo-generator/i18n/tr.ts +109 -109
  132. package/src/tool/zalgo-generator/i18n/zh.ts +109 -109
  133. package/src/tool/zalgo-generator/seo.astro +15 -15
  134. 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.25.0",
3
+ "version": "1.27.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",
@@ -1,9 +1,9 @@
1
1
  export const content = {
2
2
  slug: 'kreativitaet-freizeit',
3
3
  title: 'Kreativität und Freizeit',
4
- description: 'Werkzeuge, die inspirieren, amüsieren und kreativ machen.',
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
  };
@@ -1,9 +1,9 @@
1
1
  export const content = {
2
2
  slug: 'creativity-leisure',
3
3
  title: 'Creativity and Leisure',
4
- description: 'Tools designed to inspire, amuse, and create.',
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
  };
@@ -1,9 +1,9 @@
1
1
  export const content = {
2
2
  slug: 'creatividad-ocio',
3
3
  title: 'Creatividad y Ocio',
4
- description: 'Herramientas diseñadas para inspirar, divertir y crear.',
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
  };
@@ -1,9 +1,9 @@
1
1
  export const content = {
2
2
  slug: 'creativite-loisirs',
3
3
  title: 'Créativité et Loisirs',
4
- description: 'Des outils conçus pour inspirer, amuser et créer.',
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
  };
@@ -1,9 +1,9 @@
1
1
  export const content = {
2
2
  slug: 'kreativitas-hiburan',
3
3
  title: 'Kreativitas dan Hiburan',
4
- description: 'Alat yang dirancang untuk menginspirasi, menghibur, dan berkreasi.',
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
  };
@@ -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 progettati per ispirare, divertire e creare.',
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
  };
@@ -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
  };
@@ -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
  };
@@ -1,9 +1,9 @@
1
1
  export const content = {
2
2
  slug: 'creativiteit-vrije-tijd',
3
3
  title: 'Creativiteit en Vrije Tijd',
4
- description: 'Hulpmiddelen ontworpen om te inspireren, te vermaken en te creëren.',
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
  };
@@ -1,9 +1,9 @@
1
1
  export const content = {
2
2
  slug: 'kreatywnosc-i-rozrywka',
3
3
  title: 'Kreatywność i Rozrywka',
4
- description: 'Narzędzia zaprojektowane, aby inspirować, bawić i tworzyć.',
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 została stworzona z myślą o rozrywce i praktycznym zastosowaniu.' }
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
  };
@@ -1,9 +1,9 @@
1
1
  export const content = {
2
2
  slug: 'criatividade-lazer',
3
3
  title: 'Criatividade e Lazer',
4
- description: 'Ferramentas concebidas para inspirar, divertir e criar.',
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
  };
@@ -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
  };
@@ -1,9 +1,9 @@
1
1
  export const content = {
2
2
  slug: 'kreativitet-fritid',
3
3
  title: 'Kreativitet och Fritid',
4
- description: 'Verktyg designade för att inspirera, roa och skapa.',
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, vår samling av kreativa verktyg är designad för underhållning och praktisk användning.' }
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
  };
@@ -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 vermek, eğlendirmek ve yaratmak için tasarlanmış araçlar.',
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
  };
@@ -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
  };
@@ -12,4 +12,4 @@ const content = await creativeCategory.i18n[locale]?.();
12
12
  ---
13
13
 
14
14
  {content && <SEORenderer content={{ locale, sections: content.seo }} />}
15
-
15
+
@@ -113,4 +113,4 @@ const { categoryTitle, tools = [] } = Astro.props;
113
113
  white-space: nowrap;
114
114
  }
115
115
  </style>
116
-
116
+
@@ -140,4 +140,4 @@ const otherLocales = Object.entries(localeUrls).filter(([l]) => l !== currentLoc
140
140
  box-shadow: inset -4px -2px 0 #1e293b;
141
141
  }
142
142
  </style>
143
-
143
+
package/src/env.d.ts CHANGED
@@ -2,4 +2,4 @@ declare module '*.astro' {
2
2
  const Component: (_props: Record<string, unknown>) => unknown;
3
3
  export default Component;
4
4
  }
5
-
5
+
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  ---
3
3
  <meta http-equiv="refresh" content="0;url=/es" />
4
-
4
+
@@ -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
+ });
@@ -16,4 +16,4 @@ describe('FAQ Content Validation', () => {
16
16
  expect(TOOLS.length).toBe(0);
17
17
  });
18
18
  });
19
-
19
+
@@ -1,2 +1,2 @@
1
1
  export default function () { return null; }
2
-
2
+
@@ -45,4 +45,4 @@ describe('No H1 in Components', () => {
45
45
  });
46
46
  });
47
47
  });
48
-
48
+
@@ -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
- ).toBe(expected.seo);
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 have identical SEO section counts and types as English', async () => {
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,69 @@
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): void {
32
+ const isAsian = ASIAN_LOCALES.includes(locale);
33
+ const minLength = Math.floor(enLen * (isAsian ? 0.25 : 0.70));
34
+ const msgType = isAsian ? 'suspiciously short' : 'truncated/lazy';
35
+
36
+ expect(
37
+ localeLen,
38
+ `[LAZY SEO TRANSLATION] Tool "${toolId}" locale "${locale}" SEO text is ${msgType} (${localeLen} chars vs EN ${enLen} chars, expected min ${minLength})`,
39
+ ).toBeGreaterThanOrEqual(minLength);
40
+ }
41
+
42
+ async function auditSingleLocale(toolId: string, locale: string, loader: any, enSeoLength: number): Promise<void> {
43
+ if (locale === 'en' || !loader) return;
44
+ const content = await loader();
45
+ if (!content.seo || !Array.isArray(content.seo)) return;
46
+
47
+ checkLocaleSeoLength(toolId, locale, calculateSeoTextLength(content.seo), enSeoLength);
48
+ }
49
+
50
+ describe('SEO Translation Completeness & Laziness Audit', () => {
51
+ ALL_TOOLS.forEach(({ entry }) => {
52
+ describe(`Tool: ${entry.id}`, () => {
53
+ it('should not have lazy or truncated SEO content compared to English reference', async () => {
54
+ const enLoader = entry.i18n['en' as keyof typeof entry.i18n];
55
+ if (!enLoader) return;
56
+
57
+ const enContent = await enLoader();
58
+ if (!enContent.seo || !Array.isArray(enContent.seo)) return;
59
+
60
+ const enSeoLength = calculateSeoTextLength(enContent.seo);
61
+
62
+ for (const [locale, loader] of Object.entries(entry.i18n)) {
63
+ await auditSingleLocale(entry.id, locale, loader, enSeoLength);
64
+ }
65
+ });
66
+ });
67
+ });
68
+ });
69
+
@@ -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
+ });
@@ -67,12 +67,16 @@ function normalize(value: string): string {
67
67
  }
68
68
 
69
69
  function isTechnicalInvariant(text: string): boolean {
70
+ const ledUnitMatches = text.match(/\b\d+(?:-\d+)?\s*(?:w|lm)\b/g) ?? [];
70
71
  return [
71
72
  'data:image/svg+xml;base64',
72
73
  'background-image: url',
73
74
  '.layout-playground {',
74
75
  'const samplerate =',
75
- ].some((pattern) => text.includes(pattern)) || (text.includes('presets') && text.includes('hz'));
76
+ ].some((pattern) => text.includes(pattern)) ||
77
+ (text.includes('presets') && text.includes('hz')) ||
78
+ (text.includes('t_rectal') && text.includes('exp(-k * t)')) ||
79
+ (text.includes('led') && ledUnitMatches.length >= 3);
76
80
  }
77
81
 
78
82
  function collectString(value: string, output: string[]): void {
@@ -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} />