@jjlmoya/utils-pets 1.22.0 → 1.24.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 (103) 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 +26 -26
  9. package/src/entries.ts +12 -12
  10. package/src/tests/bibliography_wellformed_export.test.ts +46 -0
  11. package/src/tests/category_seo_quality.test.ts +74 -0
  12. package/src/tests/faq_count.test.ts +22 -22
  13. package/src/tests/locale_completeness.test.ts +30 -30
  14. package/src/tests/no_h1_in_components.test.ts +1 -1
  15. package/src/tests/pet_logic.test.ts +49 -0
  16. package/src/tests/seo_length.test.ts +54 -54
  17. package/src/tests/seo_parity.test.ts +2 -2
  18. package/src/tests/seo_translation_completeness.test.ts +18 -12
  19. package/src/tests/seo_wellformed_export.test.ts +65 -0
  20. package/src/tests/spanish_leakage.test.ts +175 -175
  21. package/src/tests/tool_validation.test.ts +137 -137
  22. package/src/tool/petAge/component.astro +3 -3
  23. package/src/tool/petAge/i18n/de.ts +184 -184
  24. package/src/tool/petAge/i18n/en.ts +184 -184
  25. package/src/tool/petAge/i18n/es.ts +184 -184
  26. package/src/tool/petAge/i18n/fr.ts +184 -184
  27. package/src/tool/petAge/i18n/id.ts +184 -184
  28. package/src/tool/petAge/i18n/it.ts +184 -184
  29. package/src/tool/petAge/i18n/ja.ts +184 -184
  30. package/src/tool/petAge/i18n/ko.ts +184 -184
  31. package/src/tool/petAge/i18n/nl.ts +184 -184
  32. package/src/tool/petAge/i18n/pl.ts +184 -184
  33. package/src/tool/petAge/i18n/pt.ts +184 -184
  34. package/src/tool/petAge/i18n/ru.ts +184 -184
  35. package/src/tool/petAge/i18n/sv.ts +184 -184
  36. package/src/tool/petAge/i18n/tr.ts +185 -185
  37. package/src/tool/petAge/i18n/zh.ts +184 -184
  38. package/src/tool/petAge/seo.astro +15 -15
  39. package/src/tool/petGestation/i18n/de.ts +0 -2
  40. package/src/tool/petGestation/i18n/en.ts +0 -2
  41. package/src/tool/petGestation/i18n/es.ts +0 -2
  42. package/src/tool/petGestation/i18n/fr.ts +0 -2
  43. package/src/tool/petGestation/i18n/id.ts +0 -2
  44. package/src/tool/petGestation/i18n/it.ts +0 -2
  45. package/src/tool/petGestation/i18n/ja.ts +0 -2
  46. package/src/tool/petGestation/i18n/ko.ts +0 -2
  47. package/src/tool/petGestation/i18n/nl.ts +0 -2
  48. package/src/tool/petGestation/i18n/pl.ts +0 -2
  49. package/src/tool/petGestation/i18n/pt.ts +0 -2
  50. package/src/tool/petGestation/i18n/ru.ts +0 -2
  51. package/src/tool/petGestation/i18n/sv.ts +0 -2
  52. package/src/tool/petGestation/i18n/tr.ts +0 -2
  53. package/src/tool/petGestation/i18n/zh.ts +0 -2
  54. package/src/tool/petGestation/ui.ts +0 -2
  55. package/src/tool/petRation/i18n/de.ts +181 -181
  56. package/src/tool/petRation/i18n/en.ts +181 -181
  57. package/src/tool/petRation/i18n/es.ts +181 -181
  58. package/src/tool/petRation/i18n/fr.ts +181 -181
  59. package/src/tool/petRation/i18n/id.ts +181 -181
  60. package/src/tool/petRation/i18n/it.ts +181 -181
  61. package/src/tool/petRation/i18n/ja.ts +181 -181
  62. package/src/tool/petRation/i18n/ko.ts +181 -181
  63. package/src/tool/petRation/i18n/nl.ts +181 -181
  64. package/src/tool/petRation/i18n/pl.ts +181 -181
  65. package/src/tool/petRation/i18n/pt.ts +181 -181
  66. package/src/tool/petRation/i18n/ru.ts +181 -181
  67. package/src/tool/petRation/i18n/sv.ts +181 -181
  68. package/src/tool/petRation/i18n/tr.ts +181 -181
  69. package/src/tool/petRation/i18n/zh.ts +181 -181
  70. package/src/tool/petRation/seo.astro +15 -15
  71. package/src/tool/petToxicity/i18n/de.ts +0 -2
  72. package/src/tool/petToxicity/i18n/en.ts +0 -2
  73. package/src/tool/petToxicity/i18n/es.ts +2 -4
  74. package/src/tool/petToxicity/i18n/fr.ts +0 -2
  75. package/src/tool/petToxicity/i18n/id.ts +0 -2
  76. package/src/tool/petToxicity/i18n/it.ts +0 -2
  77. package/src/tool/petToxicity/i18n/ja.ts +0 -2
  78. package/src/tool/petToxicity/i18n/ko.ts +0 -2
  79. package/src/tool/petToxicity/i18n/nl.ts +0 -2
  80. package/src/tool/petToxicity/i18n/pl.ts +0 -2
  81. package/src/tool/petToxicity/i18n/pt.ts +0 -2
  82. package/src/tool/petToxicity/i18n/ru.ts +0 -2
  83. package/src/tool/petToxicity/i18n/sv.ts +0 -2
  84. package/src/tool/petToxicity/i18n/tr.ts +0 -2
  85. package/src/tool/petToxicity/i18n/zh.ts +0 -2
  86. package/src/tool/petToxicity/ui.ts +0 -2
  87. package/src/tool/petWaterIntake/i18n/de.ts +0 -15
  88. package/src/tool/petWaterIntake/i18n/en.ts +0 -15
  89. package/src/tool/petWaterIntake/i18n/es.ts +2 -17
  90. package/src/tool/petWaterIntake/i18n/fr.ts +0 -15
  91. package/src/tool/petWaterIntake/i18n/id.ts +0 -15
  92. package/src/tool/petWaterIntake/i18n/it.ts +0 -15
  93. package/src/tool/petWaterIntake/i18n/ja.ts +0 -15
  94. package/src/tool/petWaterIntake/i18n/ko.ts +0 -15
  95. package/src/tool/petWaterIntake/i18n/nl.ts +0 -15
  96. package/src/tool/petWaterIntake/i18n/pl.ts +0 -15
  97. package/src/tool/petWaterIntake/i18n/pt.ts +0 -15
  98. package/src/tool/petWaterIntake/i18n/ru.ts +0 -15
  99. package/src/tool/petWaterIntake/i18n/sv.ts +0 -15
  100. package/src/tool/petWaterIntake/i18n/tr.ts +0 -15
  101. package/src/tool/petWaterIntake/i18n/zh.ts +0 -15
  102. package/src/tool/petWaterIntake/ui.ts +0 -15
  103. package/src/tools.ts +19 -19
@@ -1,139 +1,139 @@
1
- import { describe, it, expect } from 'vitest';
2
- import * as fs from 'fs';
3
- import * as path from 'path';
4
- import { ALL_TOOLS } from '../tools';
5
- import { petsCategory } from '../data';
6
- import type { ToolDefinition } from '../types';
7
-
8
- function extractSeoText(sections: any[]): string {
9
- return sections
10
- .map((section) => extractSectionText(section))
11
- .join(' ');
12
- }
13
-
14
- function extractHtmlOrText(section: any): string {
15
- const hasContent = 'html' in section || 'text' in section;
16
- const isNotTable = section.type !== 'table';
17
- return hasContent && isNotTable ? (section.html || section.text || '') : '';
18
- }
19
-
20
- function extractListItems(section: any): string {
21
- return 'items' in section && Array.isArray(section.items) ? section.items.join(' ') : '';
22
- }
23
-
24
- function extractTableRows(section: any): string {
25
- return 'rows' in section && Array.isArray(section.rows) ? section.rows.flat().join(' ') : '';
26
- }
27
-
28
- function extractSectionText(section: any): string {
29
- let text = '';
30
- text += extractHtmlOrText(section);
31
- text += extractListItems(section);
32
- text += extractTableRows(section);
33
- return text;
34
- }
35
-
36
- function countWords(text: string): number {
37
- return text
38
- .replace(/<[^>]*>/g, '')
39
- .trim()
40
- .split(/\s+/)
41
- .filter((w) => w.length > 0).length;
42
- }
43
-
44
- describe('Tool Validation Suite', () => {
45
- describe('Strict Tool Definition Validation', () => {
46
- ALL_TOOLS.forEach((tool: ToolDefinition) => {
47
- const { entry } = tool;
48
-
49
- describe(`${entry.id} structure`, () => {
50
- it('should fulfill ToolDefinition interface', () => {
51
- expect(tool.entry).toBeDefined();
52
- expect(tool.Component).toBeDefined();
53
- expect(tool.SEOComponent).toBeDefined();
54
- expect(tool.BibliographyComponent).toBeDefined();
55
- });
56
-
57
- it('should have valid ID in kebab-case', () => {
58
- expect(entry.id).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
59
- });
60
-
61
- it('should have valid icons with bg and fg', () => {
62
- expect(entry.icons.bg).toMatch(/^mdi:/);
63
- expect(entry.icons.fg).toMatch(/^mdi:/);
64
- });
65
-
66
- describe('i18n Content Validation', () => {
67
- Object.entries(entry.i18n).forEach(([locale, loader]) => {
68
- it(`should load ${locale} content and follow slug rules`, async () => {
69
- const content = await loader();
70
- expect(content.slug).toBeDefined();
71
- expect(content.slug).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
72
-
73
- if (locale === 'es') {
74
- const validSlugs = ['calculadora-edad-mascotas', 'calculadora-racion-diaria-mascotas', 'calculadora-gestacion-mascotas', 'buscador-alimentos-toxicos-perros-gatos', 'calculadora-agua-diaria-perros-gatos', 'planificador-dimensiones-transportin-mascotas'];
75
- expect(validSlugs).toContain(content.slug);
76
- }
77
- });
78
-
79
- it(`should have valid basic content in ${locale}`, async () => {
80
- const content = await loader();
81
- expect(content.title.length).toBeGreaterThan(0);
82
- expect(content.description.length).toBeGreaterThan(0);
83
- expect(Object.keys(content.ui).length).toBeGreaterThan(0);
84
- });
85
-
86
- it(`should have SEO content with > 300 words in ${locale}`, async () => {
87
- const content = await loader();
88
- const seoText = extractSeoText(content.seo);
89
- const wordCount = countWords(seoText);
90
- expect(wordCount).toBeGreaterThanOrEqual(300);
91
- });
92
- });
93
- });
94
- });
95
- });
96
- });
97
-
98
- describe('Library Registration', () => {
99
- it('should have 5 tools in ALL_TOOLS', () => {
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { ALL_TOOLS } from '../tools';
5
+ import { petsCategory } from '../data';
6
+ import type { ToolDefinition } from '../types';
7
+
8
+ function extractSeoText(sections: any[]): string {
9
+ return sections
10
+ .map((section) => extractSectionText(section))
11
+ .join(' ');
12
+ }
13
+
14
+ function extractHtmlOrText(section: any): string {
15
+ const hasContent = 'html' in section || 'text' in section;
16
+ const isNotTable = section.type !== 'table';
17
+ return hasContent && isNotTable ? (section.html || section.text || '') : '';
18
+ }
19
+
20
+ function extractListItems(section: any): string {
21
+ return 'items' in section && Array.isArray(section.items) ? section.items.join(' ') : '';
22
+ }
23
+
24
+ function extractTableRows(section: any): string {
25
+ return 'rows' in section && Array.isArray(section.rows) ? section.rows.flat().join(' ') : '';
26
+ }
27
+
28
+ function extractSectionText(section: any): string {
29
+ let text = '';
30
+ text += extractHtmlOrText(section);
31
+ text += extractListItems(section);
32
+ text += extractTableRows(section);
33
+ return text;
34
+ }
35
+
36
+ function countWords(text: string): number {
37
+ return text
38
+ .replace(/<[^>]*>/g, '')
39
+ .trim()
40
+ .split(/\s+/)
41
+ .filter((w) => w.length > 0).length;
42
+ }
43
+
44
+ describe('Tool Validation Suite', () => {
45
+ describe('Strict Tool Definition Validation', () => {
46
+ ALL_TOOLS.forEach((tool: ToolDefinition) => {
47
+ const { entry } = tool;
48
+
49
+ describe(`${entry.id} structure`, () => {
50
+ it('should fulfill ToolDefinition interface', () => {
51
+ expect(tool.entry).toBeDefined();
52
+ expect(tool.Component).toBeDefined();
53
+ expect(tool.SEOComponent).toBeDefined();
54
+ expect(tool.BibliographyComponent).toBeDefined();
55
+ });
56
+
57
+ it('should have valid ID in kebab-case', () => {
58
+ expect(entry.id).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
59
+ });
60
+
61
+ it('should have valid icons with bg and fg', () => {
62
+ expect(entry.icons.bg).toMatch(/^mdi:/);
63
+ expect(entry.icons.fg).toMatch(/^mdi:/);
64
+ });
65
+
66
+ describe('i18n Content Validation', () => {
67
+ Object.entries(entry.i18n).forEach(([locale, loader]) => {
68
+ it(`should load ${locale} content and follow slug rules`, async () => {
69
+ const content = await loader();
70
+ expect(content.slug).toBeDefined();
71
+ expect(content.slug).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
72
+
73
+ if (locale === 'es') {
74
+ const validSlugs = ['calculadora-edad-mascotas', 'calculadora-racion-diaria-mascotas', 'calculadora-gestacion-mascotas', 'buscador-alimentos-toxicos-perros-gatos', 'calculadora-agua-diaria-perros-gatos', 'planificador-dimensiones-transportin-mascotas'];
75
+ expect(validSlugs).toContain(content.slug);
76
+ }
77
+ });
78
+
79
+ it(`should have valid basic content in ${locale}`, async () => {
80
+ const content = await loader();
81
+ expect(content.title.length).toBeGreaterThan(0);
82
+ expect(content.description.length).toBeGreaterThan(0);
83
+ expect(Object.keys(content.ui).length).toBeGreaterThan(0);
84
+ });
85
+
86
+ it(`should have SEO content with > 300 words in ${locale}`, async () => {
87
+ const content = await loader();
88
+ const seoText = extractSeoText(content.seo);
89
+ const wordCount = countWords(seoText);
90
+ expect(wordCount).toBeGreaterThanOrEqual(300);
91
+ });
92
+ });
93
+ });
94
+ });
95
+ });
96
+ });
97
+
98
+ describe('Library Registration', () => {
99
+ it('should have 5 tools in ALL_TOOLS', () => {
100
100
  expect(ALL_TOOLS.length).toBe(6);
101
- });
102
-
103
- it('should have all tools in petsCategory', () => {
101
+ });
102
+
103
+ it('should have all tools in petsCategory', () => {
104
104
  expect(petsCategory.tools.length).toBe(6);
105
- ALL_TOOLS.forEach(({ entry }) => {
106
- const exists = petsCategory.tools.some((t: any) => t.id === entry.id);
107
- expect(exists).toBe(true);
108
- });
109
- });
110
- });
111
-
112
- describe('Binary Consistency', () => {
113
- ALL_TOOLS.forEach(({ entry }) => {
114
- it(`${entry.id} files should exist in the tool folder`, () => {
115
- const toolVarName = (entry.id ?? '').replace(/-([a-z])/g, (_: string, g: string) => g.toUpperCase());
116
- const toolDir = path.join('src', 'tool', toolVarName);
117
-
118
- expect(fs.existsSync(path.join(toolDir, 'component.astro'))).toBe(true);
119
- expect(fs.existsSync(path.join(toolDir, 'seo.astro'))).toBe(true);
120
- expect(fs.existsSync(path.join(toolDir, 'bibliography.astro'))).toBe(true);
121
- expect(fs.existsSync(path.join(toolDir, 'index.ts'))).toBe(true);
122
- });
123
- });
124
- });
125
-
126
- describe('Bibliography Components Structure', () => {
127
- ALL_TOOLS.forEach(({ entry }) => {
128
- it(`${entry.id} should have a standard bibliography structure`, () => {
129
- const toolVarName = (entry.id ?? '').replace(/-([a-z])/g, (_: string, g: string) => g.toUpperCase());
130
- const componentPath = path.join('src', 'tool', toolVarName, 'bibliography.astro');
131
- const content = fs.readFileSync(componentPath, 'utf-8');
132
-
133
- expect(content).toContain("import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared'");
134
- expect(content).toContain("import { bibliography } from './bibliography'");
135
- expect(content).toContain('<SharedBibliography links={bibliography} />');
136
- });
137
- });
138
- });
139
- });
105
+ ALL_TOOLS.forEach(({ entry }) => {
106
+ const exists = petsCategory.tools.some((t: any) => t.id === entry.id);
107
+ expect(exists).toBe(true);
108
+ });
109
+ });
110
+ });
111
+
112
+ describe('Binary Consistency', () => {
113
+ ALL_TOOLS.forEach(({ entry }) => {
114
+ it(`${entry.id} files should exist in the tool folder`, () => {
115
+ const toolVarName = (entry.id ?? '').replace(/-([a-z])/g, (_: string, g: string) => g.toUpperCase());
116
+ const toolDir = path.join('src', 'tool', toolVarName);
117
+
118
+ expect(fs.existsSync(path.join(toolDir, 'component.astro'))).toBe(true);
119
+ expect(fs.existsSync(path.join(toolDir, 'seo.astro'))).toBe(true);
120
+ expect(fs.existsSync(path.join(toolDir, 'bibliography.astro'))).toBe(true);
121
+ expect(fs.existsSync(path.join(toolDir, 'index.ts'))).toBe(true);
122
+ });
123
+ });
124
+ });
125
+
126
+ describe('Bibliography Components Structure', () => {
127
+ ALL_TOOLS.forEach(({ entry }) => {
128
+ it(`${entry.id} should have a standard bibliography structure`, () => {
129
+ const toolVarName = (entry.id ?? '').replace(/-([a-z])/g, (_: string, g: string) => g.toUpperCase());
130
+ const componentPath = path.join('src', 'tool', toolVarName, 'bibliography.astro');
131
+ const content = fs.readFileSync(componentPath, 'utf-8');
132
+
133
+ expect(content).toContain("import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared'");
134
+ expect(content).toContain("import { bibliography } from './bibliography'");
135
+ expect(content).toContain('<SharedBibliography links={bibliography} />');
136
+ });
137
+ });
138
+ });
139
+ });
@@ -238,6 +238,7 @@ const { ui } = Astro.props;
238
238
  state: PetAgeState,
239
239
  els: Required<ReturnType<typeof getElements>>,
240
240
  birthYearInput: HTMLInputElement,
241
+ uiData: Record<string, string>,
241
242
  ) {
242
243
  els.shareBtn?.addEventListener("click", async () => {
243
244
  if (!state.birthYear) {
@@ -253,7 +254,7 @@ const { ui } = Astro.props;
253
254
  const url = generateShareUrl(state);
254
255
  try {
255
256
  await navigator.clipboard.writeText(url);
256
- if (els.shareText) els.shareText.textContent = "¡Copiado!";
257
+ if (els.shareText) els.shareText.textContent = uiData.shareSuccess;
257
258
  if (els.shareBtn) els.shareBtn.classList.add("age-btn-success");
258
259
  setTimeout(() => (window.location.href = url), 800);
259
260
  } catch {
@@ -292,7 +293,7 @@ const { ui } = Astro.props;
292
293
  setupPetTypeListener(state, els, onUpdate);
293
294
  setupSizeListener(state, els, onUpdate);
294
295
  setupBirthYearListener(state, birthYearInput, currentYear, onUpdate);
295
- setupShareListener(state, els, birthYearInput);
296
+ setupShareListener(state, els, birthYearInput, uiData);
296
297
 
297
298
  onUpdate();
298
299
  setTimeout(() => els.loader?.classList.add("hidden"), 50);
@@ -305,4 +306,3 @@ const { ui } = Astro.props;
305
306
  }
306
307
  document.addEventListener("astro:page-load", initPetAge);
307
308
  </script>
308
-