@jjlmoya/utils-pets 1.21.0 → 1.23.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 (138) hide show
  1. package/package.json +2 -2
  2. package/src/category/i18n/de.ts +1 -1
  3. package/src/category/i18n/en.ts +2 -2
  4. package/src/category/i18n/es.ts +1 -1
  5. package/src/category/i18n/fr.ts +1 -1
  6. package/src/category/i18n/pt.ts +1 -1
  7. package/src/category/i18n/tr.ts +1 -1
  8. package/src/category/index.ts +28 -27
  9. package/src/entries.ts +16 -13
  10. package/src/index.ts +1 -0
  11. package/src/tests/bibliography_wellformed_export.test.ts +46 -0
  12. package/src/tests/category_seo_quality.test.ts +74 -0
  13. package/src/tests/faq_count.test.ts +22 -22
  14. package/src/tests/i18n_coverage.test.ts +5 -2
  15. package/src/tests/locale_completeness.test.ts +31 -31
  16. package/src/tests/no_h1_in_components.test.ts +1 -1
  17. package/src/tests/pet_logic.test.ts +49 -0
  18. package/src/tests/qa-test-helpers.ts +32 -0
  19. package/src/tests/qa_bibliography_links.test.ts +40 -0
  20. package/src/tests/qa_claim_evidence.test.ts +69 -0
  21. package/src/tests/qa_logic_reference_coverage.test.ts +46 -0
  22. package/src/tests/qa_runtime_i18n.test.ts +100 -0
  23. package/src/tests/seo_length.test.ts +54 -54
  24. package/src/tests/seo_parity.test.ts +2 -2
  25. package/src/tests/seo_wellformed_export.test.ts +65 -0
  26. package/src/tests/spanish_leakage.test.ts +175 -175
  27. package/src/tests/tool_validation.test.ts +139 -139
  28. package/src/tool/petAge/component.astro +3 -3
  29. package/src/tool/petAge/i18n/de.ts +184 -184
  30. package/src/tool/petAge/i18n/en.ts +184 -184
  31. package/src/tool/petAge/i18n/es.ts +184 -184
  32. package/src/tool/petAge/i18n/fr.ts +184 -184
  33. package/src/tool/petAge/i18n/id.ts +184 -184
  34. package/src/tool/petAge/i18n/it.ts +184 -184
  35. package/src/tool/petAge/i18n/ja.ts +184 -184
  36. package/src/tool/petAge/i18n/ko.ts +184 -184
  37. package/src/tool/petAge/i18n/nl.ts +184 -184
  38. package/src/tool/petAge/i18n/pl.ts +184 -184
  39. package/src/tool/petAge/i18n/pt.ts +184 -184
  40. package/src/tool/petAge/i18n/ru.ts +184 -184
  41. package/src/tool/petAge/i18n/sv.ts +184 -184
  42. package/src/tool/petAge/i18n/tr.ts +185 -185
  43. package/src/tool/petAge/i18n/zh.ts +184 -184
  44. package/src/tool/petAge/seo.astro +15 -15
  45. package/src/tool/petCarrierCrateSizePlanner/bibliography.astro +6 -0
  46. package/src/tool/petCarrierCrateSizePlanner/bibliography.ts +12 -0
  47. package/src/tool/petCarrierCrateSizePlanner/component.astro +89 -0
  48. package/src/tool/petCarrierCrateSizePlanner/controller.ts +128 -0
  49. package/src/tool/petCarrierCrateSizePlanner/dom-views.ts +96 -0
  50. package/src/tool/petCarrierCrateSizePlanner/entry.ts +27 -0
  51. package/src/tool/petCarrierCrateSizePlanner/evaluator.ts +14 -0
  52. package/src/tool/petCarrierCrateSizePlanner/i18n/de.ts +171 -0
  53. package/src/tool/petCarrierCrateSizePlanner/i18n/en.ts +171 -0
  54. package/src/tool/petCarrierCrateSizePlanner/i18n/es.ts +171 -0
  55. package/src/tool/petCarrierCrateSizePlanner/i18n/fr.ts +171 -0
  56. package/src/tool/petCarrierCrateSizePlanner/i18n/id.ts +171 -0
  57. package/src/tool/petCarrierCrateSizePlanner/i18n/it.ts +171 -0
  58. package/src/tool/petCarrierCrateSizePlanner/i18n/ja.ts +171 -0
  59. package/src/tool/petCarrierCrateSizePlanner/i18n/ko.ts +171 -0
  60. package/src/tool/petCarrierCrateSizePlanner/i18n/nl.ts +171 -0
  61. package/src/tool/petCarrierCrateSizePlanner/i18n/pl.ts +171 -0
  62. package/src/tool/petCarrierCrateSizePlanner/i18n/pt.ts +171 -0
  63. package/src/tool/petCarrierCrateSizePlanner/i18n/ru.ts +171 -0
  64. package/src/tool/petCarrierCrateSizePlanner/i18n/sv.ts +171 -0
  65. package/src/tool/petCarrierCrateSizePlanner/i18n/tr.ts +171 -0
  66. package/src/tool/petCarrierCrateSizePlanner/i18n/zh.ts +171 -0
  67. package/src/tool/petCarrierCrateSizePlanner/index.ts +12 -0
  68. package/src/tool/petCarrierCrateSizePlanner/logic.test.ts +49 -0
  69. package/src/tool/petCarrierCrateSizePlanner/logic.ts +77 -0
  70. package/src/tool/petCarrierCrateSizePlanner/pet-carrier-crate-size-planner.css +639 -0
  71. package/src/tool/petCarrierCrateSizePlanner/seo.astro +14 -0
  72. package/src/tool/petCarrierCrateSizePlanner/storage.ts +31 -0
  73. package/src/tool/petCarrierCrateSizePlanner/ui.ts +59 -0
  74. package/src/tool/petGestation/i18n/de.ts +0 -2
  75. package/src/tool/petGestation/i18n/en.ts +0 -2
  76. package/src/tool/petGestation/i18n/es.ts +0 -2
  77. package/src/tool/petGestation/i18n/fr.ts +0 -2
  78. package/src/tool/petGestation/i18n/id.ts +0 -2
  79. package/src/tool/petGestation/i18n/it.ts +0 -2
  80. package/src/tool/petGestation/i18n/ja.ts +0 -2
  81. package/src/tool/petGestation/i18n/ko.ts +0 -2
  82. package/src/tool/petGestation/i18n/nl.ts +0 -2
  83. package/src/tool/petGestation/i18n/pl.ts +0 -2
  84. package/src/tool/petGestation/i18n/pt.ts +0 -2
  85. package/src/tool/petGestation/i18n/ru.ts +0 -2
  86. package/src/tool/petGestation/i18n/sv.ts +0 -2
  87. package/src/tool/petGestation/i18n/tr.ts +0 -2
  88. package/src/tool/petGestation/i18n/zh.ts +0 -2
  89. package/src/tool/petGestation/ui.ts +0 -2
  90. package/src/tool/petRation/i18n/de.ts +181 -181
  91. package/src/tool/petRation/i18n/en.ts +181 -181
  92. package/src/tool/petRation/i18n/es.ts +181 -181
  93. package/src/tool/petRation/i18n/fr.ts +181 -181
  94. package/src/tool/petRation/i18n/id.ts +181 -181
  95. package/src/tool/petRation/i18n/it.ts +181 -181
  96. package/src/tool/petRation/i18n/ja.ts +181 -181
  97. package/src/tool/petRation/i18n/ko.ts +181 -181
  98. package/src/tool/petRation/i18n/nl.ts +181 -181
  99. package/src/tool/petRation/i18n/pl.ts +181 -181
  100. package/src/tool/petRation/i18n/pt.ts +181 -181
  101. package/src/tool/petRation/i18n/ru.ts +181 -181
  102. package/src/tool/petRation/i18n/sv.ts +181 -181
  103. package/src/tool/petRation/i18n/tr.ts +181 -181
  104. package/src/tool/petRation/i18n/zh.ts +181 -181
  105. package/src/tool/petRation/seo.astro +15 -15
  106. package/src/tool/petToxicity/i18n/de.ts +0 -2
  107. package/src/tool/petToxicity/i18n/en.ts +0 -2
  108. package/src/tool/petToxicity/i18n/es.ts +2 -4
  109. package/src/tool/petToxicity/i18n/fr.ts +0 -2
  110. package/src/tool/petToxicity/i18n/id.ts +0 -2
  111. package/src/tool/petToxicity/i18n/it.ts +0 -2
  112. package/src/tool/petToxicity/i18n/ja.ts +0 -2
  113. package/src/tool/petToxicity/i18n/ko.ts +0 -2
  114. package/src/tool/petToxicity/i18n/nl.ts +0 -2
  115. package/src/tool/petToxicity/i18n/pl.ts +0 -2
  116. package/src/tool/petToxicity/i18n/pt.ts +0 -2
  117. package/src/tool/petToxicity/i18n/ru.ts +0 -2
  118. package/src/tool/petToxicity/i18n/sv.ts +0 -2
  119. package/src/tool/petToxicity/i18n/tr.ts +0 -2
  120. package/src/tool/petToxicity/i18n/zh.ts +0 -2
  121. package/src/tool/petToxicity/ui.ts +0 -2
  122. package/src/tool/petWaterIntake/i18n/de.ts +0 -15
  123. package/src/tool/petWaterIntake/i18n/en.ts +0 -15
  124. package/src/tool/petWaterIntake/i18n/es.ts +2 -17
  125. package/src/tool/petWaterIntake/i18n/fr.ts +0 -15
  126. package/src/tool/petWaterIntake/i18n/id.ts +0 -15
  127. package/src/tool/petWaterIntake/i18n/it.ts +0 -15
  128. package/src/tool/petWaterIntake/i18n/ja.ts +0 -15
  129. package/src/tool/petWaterIntake/i18n/ko.ts +0 -15
  130. package/src/tool/petWaterIntake/i18n/nl.ts +0 -15
  131. package/src/tool/petWaterIntake/i18n/pl.ts +0 -15
  132. package/src/tool/petWaterIntake/i18n/pt.ts +0 -15
  133. package/src/tool/petWaterIntake/i18n/ru.ts +0 -15
  134. package/src/tool/petWaterIntake/i18n/sv.ts +0 -15
  135. package/src/tool/petWaterIntake/i18n/tr.ts +0 -15
  136. package/src/tool/petWaterIntake/i18n/zh.ts +0 -15
  137. package/src/tool/petWaterIntake/ui.ts +0 -15
  138. package/src/tools.ts +22 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-pets",
3
- "version": "1.21.0",
3
+ "version": "1.23.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",
@@ -3,7 +3,7 @@ import type { CategoryLocaleContent } from '../../types';
3
3
  export const content: CategoryLocaleContent = {
4
4
  slug: 'haustiere',
5
5
  title: 'Haustier Tools',
6
- description: 'Kostenlose Rechner und Dienstprogramme für Haustierbesitzer. Finden Sie das wahre Alter Ihres Hundes oder Ihrer Katze heraus und berechnen Sie die tägliche Futterration mit tierärztlicher Präzision.',
6
+ description: 'Kostenlose Rechner für Haustierbesitzer: Hunde- und Katzenalter sowie tägliche Futterrationen einfach einschätzen.',
7
7
  seo: [
8
8
  { type: 'title', text: 'Tierernährung und Wohlbefinden: Wissenschaft im Dienste Ihres Haustieres', level: 2 },
9
9
  {
@@ -2,13 +2,13 @@ import type { CategoryLocaleContent } from '../../types';
2
2
 
3
3
  export const content: CategoryLocaleContent = {
4
4
  slug: 'pets',
5
- title: 'Pet Tools',
5
+ title: 'Pet Care Calculators',
6
6
  description: 'Free calculators and utilities for pet owners. Find out your dog or cat\'s real age and calculate their daily ration with veterinary precision.',
7
7
  seo: [
8
8
  { type: 'title', text: 'Animal Nutrition and Wellbeing: Science in the Service of Your Pet', level: 2 },
9
9
  {
10
10
  type: 'paragraph',
11
- html: 'Caring for a dog, cat, or rabbit is much more than giving them food and affection. It involves understanding their biology, their aging rhythm, and their specific energy needs. In this section we offer <strong>free veterinary calculators</strong> that apply validated scientific formulas to help you make better decisions about your companion\'s health.',
11
+ html: 'Caring for a dog, cat, or rabbit involves understanding biology, aging and energy needs. Our <strong>free veterinary calculators</strong> use published formulas to support informed decisions about your companion\'s health.',
12
12
  },
13
13
  {
14
14
  type: 'paragraph',
@@ -8,7 +8,7 @@ export const content: CategoryLocaleContent = {
8
8
  { type: 'title', text: 'Nutrición y Bienestar Animal: Ciencia al Servicio de tu Mascota', level: 2 },
9
9
  {
10
10
  type: 'paragraph',
11
- html: 'Cuidar a un perro, un gato o un conejo es mucho más que darles comida y cariño. Implica entender su biología, su ritmo de envejecimiento y sus necesidades energéticas concretas. En esta sección ofrecemos <strong>calculadoras veterinarias gratuitas</strong> que aplican fórmulas científicas validadas para ayudarte a tomar mejores decisiones sobre la salud de tu compañero.',
11
+ html: 'Cuidar de un perro, gato o conejo implica entender su biología, envejecimiento y necesidades energéticas. Nuestras <strong>calculadoras veterinarias gratuitas</strong> usan fórmulas publicadas para orientar decisiones sobre su salud.',
12
12
  },
13
13
  {
14
14
  type: 'paragraph',
@@ -8,7 +8,7 @@ export const content: CategoryLocaleContent = {
8
8
  { type: 'title', text: 'Nutrition et Bien-être Animal: La Science au Service de votre Animal', level: 2 },
9
9
  {
10
10
  type: 'paragraph',
11
- html: 'Prendre soin d\'un chien, d\'un chat ou d\'un lapin va bien au-delà de leur donner de la nourriture et de l\'affection. Cela implique de comprendre leur biologie, leur rythme de vieillissement et leurs besoins énergétiques spécifiques. Dans cette section, nous proposons des <strong>calculatrices vétérinaires gratuites</strong> qui appliquent des formules scientifiques validées pour vous aider à prendre de meilleures décisions concernant la santé de votre compagnon.',
11
+ html: 'Prendre soin d\'un chien, d\'un chat ou d\'un lapin implique de comprendre sa biologie, son vieillissement et ses besoins énergétiques. Nos <strong>calculatrices vétérinaires gratuites</strong> utilisent des formules publiées pour éclairer vos décisions.',
12
12
  },
13
13
  {
14
14
  type: 'paragraph',
@@ -8,7 +8,7 @@ export const content: CategoryLocaleContent = {
8
8
  { type: 'title', text: 'Nutrição e Bem-estar Animal: Ciência ao Serviço do Seu Animal', level: 2 },
9
9
  {
10
10
  type: 'paragraph',
11
- html: 'Cuidar de um cão, gato ou coelho é muito mais do que dar-lhes comida e afeto. Envolve compreender a sua biologia, o seu ritmo de envelhecimento e as suas necessidades energéticas específicas. Nesta secção oferecemos <strong>calculadoras veterinárias gratuitas</strong> que aplicam fórmulas científicas validadas para o ajudar a tomar melhores decisões sobre a saúde do seu companheiro.',
11
+ html: 'Cuidar de um cão, gato ou coelho implica compreender a sua biologia, envelhecimento e necessidades energéticas. As nossas <strong>calculadoras veterinárias gratuitas</strong> usam fórmulas publicadas para apoiar decisões informadas.',
12
12
  },
13
13
  {
14
14
  type: 'paragraph',
@@ -3,7 +3,7 @@ import type { CategoryLocaleContent } from '../../types';
3
3
  export const content: CategoryLocaleContent = {
4
4
  slug: 'evcil-hayvanlar',
5
5
  title: 'Evcil Hayvan Araçları',
6
- description: 'Evcil hayvan sahipleri için ücretsiz hesaplayıcılar ve yardımcı programlar. Köpeğinizin veya kedinizin gerçek yaşını öğrenin ve günlük mama miktarını veteriner hassasiyetiyle hesaplayın.',
6
+ description: 'Evcil hayvan sahipleri için ücretsiz hesaplayıcılar: köpek ve kedi yaşı ile günlük mama miktarını tahmin edin.',
7
7
  seo: [
8
8
  { type: 'title', text: 'Hayvan Beslenmesi ve Refahı: Evcil Hayvanınızın Hizmetinde Bilim', level: 2 },
9
9
  {
@@ -1,28 +1,29 @@
1
- import type { PetCategoryEntry } from '../types';
2
- import { petAge } from '../tool/petAge/entry';
3
- import { petRation } from '../tool/petRation/entry';
4
- import { petGestation } from '../tool/petGestation/entry';
5
- import { petToxicity } from '../tool/petToxicity/entry';
1
+ import type { PetCategoryEntry } from '../types';
2
+ import { petAge } from '../tool/petAge/entry';
3
+ import { petRation } from '../tool/petRation/entry';
4
+ import { petGestation } from '../tool/petGestation/entry';
5
+ import { petToxicity } from '../tool/petToxicity/entry';
6
6
  import { petWaterIntake } from '../tool/petWaterIntake/entry';
7
-
8
- export const petsCategory: PetCategoryEntry = {
9
- icon: 'mdi:paw',
10
- tools: [petAge, petRation, petGestation, petToxicity, petWaterIntake],
11
- i18n: {
12
- en: () => import('./i18n/en').then((m) => m.content),
13
- es: () => import('./i18n/es').then((m) => m.content),
14
- fr: () => import('./i18n/fr').then((m) => m.content),
15
- de: () => import('./i18n/de').then((m) => m.content),
16
- id: () => import('./i18n/id').then((m) => m.content),
17
- it: () => import('./i18n/it').then((m) => m.content),
18
- ja: () => import('./i18n/ja').then((m) => m.content),
19
- ko: () => import('./i18n/ko').then((m) => m.content),
20
- nl: () => import('./i18n/nl').then((m) => m.content),
21
- pl: () => import('./i18n/pl').then((m) => m.content),
22
- pt: () => import('./i18n/pt').then((m) => m.content),
23
- ru: () => import('./i18n/ru').then((m) => m.content),
24
- sv: () => import('./i18n/sv').then((m) => m.content),
25
- tr: () => import('./i18n/tr').then((m) => m.content),
26
- zh: () => import('./i18n/zh').then((m) => m.content),
27
- },
28
- };
7
+ import { petCarrierCrateSizePlanner } from '../tool/petCarrierCrateSizePlanner/entry';
8
+
9
+ export const petsCategory: PetCategoryEntry = {
10
+ icon: 'mdi:paw',
11
+ tools: [petAge, petRation, petGestation, petToxicity, petWaterIntake, petCarrierCrateSizePlanner],
12
+ i18n: {
13
+ en: () => import('./i18n/en').then((m) => m.content),
14
+ es: () => import('./i18n/es').then((m) => m.content),
15
+ fr: () => import('./i18n/fr').then((m) => m.content),
16
+ de: () => import('./i18n/de').then((m) => m.content),
17
+ id: () => import('./i18n/id').then((m) => m.content),
18
+ it: () => import('./i18n/it').then((m) => m.content),
19
+ ja: () => import('./i18n/ja').then((m) => m.content),
20
+ ko: () => import('./i18n/ko').then((m) => m.content),
21
+ nl: () => import('./i18n/nl').then((m) => m.content),
22
+ pl: () => import('./i18n/pl').then((m) => m.content),
23
+ pt: () => import('./i18n/pt').then((m) => m.content),
24
+ ru: () => import('./i18n/ru').then((m) => m.content),
25
+ sv: () => import('./i18n/sv').then((m) => m.content),
26
+ tr: () => import('./i18n/tr').then((m) => m.content),
27
+ zh: () => import('./i18n/zh').then((m) => m.content),
28
+ },
29
+ };
package/src/entries.ts CHANGED
@@ -1,15 +1,18 @@
1
- export { petAge } from './tool/petAge/entry';
2
- export type { PetAgeUI, PetAgeLocaleContent } from './tool/petAge/entry';
3
- export { petRation } from './tool/petRation/entry';
4
- export type { PetRationUI, PetRationLocaleContent } from './tool/petRation/entry';
5
- export { petToxicity } from './tool/petToxicity/entry';
6
- export type { PetToxicityUI, PetToxicityLocaleContent } from './tool/petToxicity/entry';
7
- export { petWaterIntake } from './tool/petWaterIntake/entry';
1
+ export { petAge } from './tool/petAge/entry';
2
+ export type { PetAgeUI, PetAgeLocaleContent } from './tool/petAge/entry';
3
+ export { petRation } from './tool/petRation/entry';
4
+ export type { PetRationUI, PetRationLocaleContent } from './tool/petRation/entry';
5
+ export { petToxicity } from './tool/petToxicity/entry';
6
+ export type { PetToxicityUI, PetToxicityLocaleContent } from './tool/petToxicity/entry';
7
+ export { petWaterIntake } from './tool/petWaterIntake/entry';
8
8
  export type { PetWaterIntakeUI, PetWaterIntakeLocaleContent } from './tool/petWaterIntake/entry';
9
- export { petsCategory } from './category';
10
- import { petAge } from './tool/petAge/entry';
11
- import { petRation } from './tool/petRation/entry';
12
- import { petGestation } from './tool/petGestation/entry';
13
- import { petToxicity } from './tool/petToxicity/entry';
9
+ export { petCarrierCrateSizePlanner } from './tool/petCarrierCrateSizePlanner/entry';
10
+ export type { PetCarrierCrateSizePlannerUI, PetCarrierCrateSizePlannerLocaleContent } from './tool/petCarrierCrateSizePlanner/entry';
11
+ export { petsCategory } from './category';
12
+ import { petAge } from './tool/petAge/entry';
13
+ import { petRation } from './tool/petRation/entry';
14
+ import { petGestation } from './tool/petGestation/entry';
15
+ import { petToxicity } from './tool/petToxicity/entry';
14
16
  import { petWaterIntake } from './tool/petWaterIntake/entry';
15
- export const ALL_ENTRIES = [petAge, petRation, petGestation, petToxicity, petWaterIntake];
17
+ import { petCarrierCrateSizePlanner } from './tool/petCarrierCrateSizePlanner/entry';
18
+ export const ALL_ENTRIES = [petAge, petRation, petGestation, petToxicity, petWaterIntake, petCarrierCrateSizePlanner];
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { petAge, PET_AGE_TOOL } from './tool/petAge';
2
2
  export { petRation, PET_RATION_TOOL } from './tool/petRation';
3
3
  export { petGestation, PET_GESTATION_TOOL } from './tool/petGestation';
4
+ export { petCarrierCrateSizePlanner, PET_CARRIER_CRATE_SIZE_PLANNER_TOOL } from './tool/petCarrierCrateSizePlanner';
4
5
 
5
6
  export { petsCategory } from './category';
6
7
  export const PetsCategorySEO = () => import('./category/seo.astro').then((m) => m.default);
@@ -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,23 +1,23 @@
1
- import { describe, it, expect } from 'vitest';
2
- import * as DATA from '../data';
3
-
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as DATA from '../data';
3
+
4
4
  const TOOLS = [DATA.petAge, DATA.petRation, DATA.petGestation, DATA.petToxicity];
5
-
6
- describe('FAQ Content Validation', () => {
7
- TOOLS.forEach((entry) => {
8
- describe(`Tool: ${entry.id}`, () => {
9
- Object.keys(entry.i18n).forEach((locale) => {
10
- it(`${locale}: should have at least 3 FAQ items`, async () => {
11
- const loader = (entry.i18n as any)[locale];
12
- const content = await loader();
13
-
14
- if (!content.faq) {
15
- throw new Error(`Tool ${entry.id} (${locale}) is missing the 'faq' property entirely.`);
16
- }
17
-
18
- expect(content.faq.length, `Tool ${entry.id} (${locale}) has only ${content.faq.length} FAQs, minimum 3 required.`).toBeGreaterThanOrEqual(3);
19
- });
20
- });
21
- });
22
- });
23
- });
5
+
6
+ describe('FAQ Content Validation', () => {
7
+ TOOLS.forEach((entry) => {
8
+ describe(`Tool: ${entry.id}`, () => {
9
+ Object.keys(entry.i18n).forEach((locale) => {
10
+ it(`${locale}: should have at least 3 FAQ items`, async () => {
11
+ const loader = (entry.i18n as any)[locale];
12
+ const content = await loader();
13
+
14
+ if (!content.faq) {
15
+ throw new Error(`Tool ${entry.id} (${locale}) is missing the 'faq' property entirely.`);
16
+ }
17
+
18
+ expect(content.faq.length, `Tool ${entry.id} (${locale}) has only ${content.faq.length} FAQs, minimum 3 required.`).toBeGreaterThanOrEqual(3);
19
+ });
20
+ });
21
+ });
22
+ });
23
+ });
@@ -4,6 +4,7 @@ import { ALL_TOOLS } from '../tools';
4
4
  const EXPECTED_LOCALES = [
5
5
  'de', 'en', 'es', 'fr', 'id', 'it', 'ja', 'ko', 'nl', 'pl', 'pt', 'ru', 'sv', 'tr', 'zh'
6
6
  ];
7
+ const ENGLISH_FIRST_TOOLS = new Set(['pet-carrier-crate-size-planner']);
7
8
 
8
9
  describe('I18n Coverage Validation', () => {
9
10
  it('all tools should be registered', () => {
@@ -12,9 +13,11 @@ describe('I18n Coverage Validation', () => {
12
13
 
13
14
  ALL_TOOLS.forEach(({ entry }: { entry: any }) => {
14
15
  describe(`Tool: ${entry.id}`, () => {
16
+ const expectedLocales = ENGLISH_FIRST_TOOLS.has(entry.id) ? ['en'] : EXPECTED_LOCALES;
17
+
15
18
  it('should have all 15 required locales', () => {
16
19
  const registeredLocales = Object.keys(entry.i18n);
17
- EXPECTED_LOCALES.forEach((locale) => {
20
+ expectedLocales.forEach((locale) => {
18
21
  expect(
19
22
  registeredLocales,
20
23
  `Tool "${entry.id}" is missing locale "${locale}"`,
@@ -23,7 +26,7 @@ describe('I18n Coverage Validation', () => {
23
26
  });
24
27
 
25
28
  it('all locale loaders should be functions', () => {
26
- EXPECTED_LOCALES.forEach((locale) => {
29
+ expectedLocales.forEach((locale) => {
27
30
  const loader = entry.i18n[locale as keyof typeof entry.i18n];
28
31
  expect(
29
32
  typeof loader,
@@ -1,31 +1,31 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { ALL_TOOLS } from '../tools';
3
- import type { ToolLocaleContent } from '../types';
4
-
5
- describe('Locale Completeness Validation', () => {
6
- ALL_TOOLS.forEach((tool) => {
7
- describe(`Tool: ${tool.entry.id}`, () => {
8
- Object.keys(tool.entry.i18n).forEach((locale) => {
9
- describe(`Locale: ${locale}`, () => {
10
- it('faq should be defined', async () => {
11
- const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
12
- const content = (await loader?.()) as ToolLocaleContent;
13
-
14
- expect(Array.isArray(content.faq)).toBe(true);
15
- });
16
-
17
- it('bibliography should be defined', async () => {
18
- const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
19
- const content = (await loader?.()) as ToolLocaleContent;
20
-
21
- expect(Array.isArray(content.bibliography)).toBe(true);
22
- });
23
- });
24
- });
25
- });
26
- });
27
-
28
- it('all tools registered', () => {
29
- expect(ALL_TOOLS.length).toBe(5);
30
- });
31
- });
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+ import type { ToolLocaleContent } from '../types';
4
+
5
+ describe('Locale Completeness Validation', () => {
6
+ ALL_TOOLS.forEach((tool) => {
7
+ describe(`Tool: ${tool.entry.id}`, () => {
8
+ Object.keys(tool.entry.i18n).forEach((locale) => {
9
+ describe(`Locale: ${locale}`, () => {
10
+ it('faq should be defined', async () => {
11
+ const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
12
+ const content = (await loader?.()) as ToolLocaleContent;
13
+
14
+ expect(Array.isArray(content.faq)).toBe(true);
15
+ });
16
+
17
+ it('bibliography should be defined', async () => {
18
+ const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
19
+ const content = (await loader?.()) as ToolLocaleContent;
20
+
21
+ expect(Array.isArray(content.bibliography)).toBe(true);
22
+ });
23
+ });
24
+ });
25
+ });
26
+ });
27
+
28
+ it('all tools registered', () => {
29
+ expect(ALL_TOOLS.length).toBe(6);
30
+ });
31
+ });
@@ -45,4 +45,4 @@ describe('No H1 in Components', () => {
45
45
  });
46
46
  });
47
47
  });
48
-
48
+
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ calculateCatHumanAge,
4
+ calculateDogHumanAge,
5
+ calculatePetHumanAge,
6
+ getCatStageKey,
7
+ getDogMilestone,
8
+ } from '../tool/petAge/logic';
9
+ import {
10
+ calculateDER,
11
+ calculateRation,
12
+ calculateRER,
13
+ getStatusKey,
14
+ } from '../tool/petRation/logic';
15
+
16
+ describe('pet age reference calculations', () => {
17
+ it('uses the documented first-year and second-year anchors', () => {
18
+ expect(calculateDogHumanAge(1, 'small')).toBe(15);
19
+ expect(calculateDogHumanAge(2, 'small')).toBe(24);
20
+ expect(calculateCatHumanAge(1)).toBe(15);
21
+ expect(calculateCatHumanAge(2)).toBe(24);
22
+ });
23
+
24
+ it('handles zero and size-specific adult progression', () => {
25
+ expect(calculatePetHumanAge('dog', 0, 'giant')).toBe(0);
26
+ expect(calculatePetHumanAge('cat', 0)).toBe(0);
27
+ expect(calculateDogHumanAge(3, 'small')).toBe(29);
28
+ expect(calculateDogHumanAge(3, 'giant')).toBe(32);
29
+ expect(getCatStageKey(80)).toBe('stageCatVenerable');
30
+ expect(getDogMilestone(8)).toBe('Chequeo Geriátrico Semestral');
31
+ });
32
+ });
33
+
34
+ describe('pet ration reference calculations', () => {
35
+ it('calculates RER and DER using the selected profile', () => {
36
+ expect(calculateRER(10)).toBeCloseTo(393.64, 1);
37
+ expect(calculateDER(400, 'dog', 'adult', 'low')).toBe(480);
38
+ expect(calculateDER(400, 'dog', 'adult', 'high')).toBe(800);
39
+ });
40
+
41
+ it('keeps dry and wet ration outputs non-negative and preserves the split', () => {
42
+ const ration = calculateRation(700, 0.4);
43
+ expect(ration.dryGrams).toBe(120);
44
+ expect(ration.wetGrams).toBe(280);
45
+ expect(calculateRation(0, 0)).toEqual({ dryGrams: 0, wetGrams: 0 });
46
+ expect(getStatusKey('dog', 'puppy', 5)).toBe('statusPuppy');
47
+ expect(getStatusKey('dog', 'adult', 50)).toBe('statusLargeDog');
48
+ });
49
+ });
@@ -0,0 +1,32 @@
1
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+
4
+ export const repositoryRoot = process.cwd();
5
+ export const toolRoot = join(repositoryRoot, 'src', 'tool');
6
+
7
+ export function listToolDirectories(): string[] {
8
+ return readdirSync(toolRoot)
9
+ .map((name) => join(toolRoot, name))
10
+ .filter((path) => statSync(path).isDirectory());
11
+ }
12
+
13
+ export function findFiles(directory: string, extensions: string[]): string[] {
14
+ const files: string[] = [];
15
+
16
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
17
+ const path = join(directory, entry.name);
18
+ if (entry.isDirectory()) files.push(...findFiles(path, extensions));
19
+ else if (extensions.some((extension) => entry.name.endsWith(extension))) files.push(path);
20
+ }
21
+
22
+ return files;
23
+ }
24
+
25
+ export function read(path: string): string {
26
+ return readFileSync(path, 'utf8');
27
+ }
28
+
29
+ export function displayPath(path: string): string {
30
+ return relative(repositoryRoot, path).replace(/\\/g, '/');
31
+ }
32
+
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+ import type { ToolLocaleContent } from '../types';
4
+
5
+ interface LinkFailure {
6
+ tool: string;
7
+ message: string;
8
+ }
9
+
10
+ function checkBibliographyEntry(tool: string, entry: { url: string }, seen: Set<string>): LinkFailure[] {
11
+ const failures: LinkFailure[] = [];
12
+ let url: URL;
13
+ try {
14
+ url = new URL(entry.url);
15
+ } catch {
16
+ return [{ tool, message: `invalid URL: ${entry.url}` }];
17
+ }
18
+
19
+ if (url.protocol !== 'https:') failures.push({ tool, message: `non-HTTPS URL: ${entry.url}` });
20
+ if (url.pathname === '/' && !url.search && !url.hash) failures.push({ tool, message: `generic homepage, cite the exact document: ${entry.url}` });
21
+ if (seen.has(url.href)) failures.push({ tool, message: `duplicate source URL: ${entry.url}` });
22
+ seen.add(url.href);
23
+ return failures;
24
+ }
25
+
26
+ async function checkToolBibliography(tool: (typeof ALL_TOOLS)[number]): Promise<LinkFailure[]> {
27
+ const loader = tool.entry.i18n.en;
28
+ if (!loader) return [{ tool: tool.entry.id, message: 'English locale loader is missing' }];
29
+ const content = (await loader()) as ToolLocaleContent;
30
+ const seen = new Set<string>();
31
+ return (content.bibliography ?? []).flatMap((entry) => checkBibliographyEntry(tool.entry.id, entry, seen));
32
+ }
33
+
34
+ describe('QA: bibliography links are specific and usable', () => {
35
+ it('uses unique HTTPS links to exact source pages instead of generic homepages', async () => {
36
+ const failures = (await Promise.all(ALL_TOOLS.map(checkToolBibliography))).flat();
37
+ const messages = failures.map(({ tool, message }) => `${tool}: ${message}`);
38
+ expect(messages, `Bibliography hygiene failures:\n${messages.join('\n')}`).toEqual([]);
39
+ });
40
+ });