@jjlmoya/utils-developer 1.30.0 → 1.31.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 (140) hide show
  1. package/package.json +70 -70
  2. package/src/category/seo.astro +1 -1
  3. package/src/components/PreviewNavSidebar.astro +1 -1
  4. package/src/components/PreviewToolbar.astro +1 -1
  5. package/src/data.ts +1 -1
  6. package/src/env.d.ts +1 -1
  7. package/src/layouts/PreviewLayout.astro +1 -1
  8. package/src/pages/[locale]/[slug].astro +162 -162
  9. package/src/pages/index.astro +1 -1
  10. package/src/tests/entry_contract.test.ts +8 -2
  11. package/src/tests/faq_count.test.ts +1 -1
  12. package/src/tests/i18n_coverage.test.ts +2 -6
  13. package/src/tests/inverted_punctuation.test.ts +113 -113
  14. package/src/tests/mocks/astro_mock.js +1 -1
  15. package/src/tests/no_h1_in_components.test.ts +1 -1
  16. package/src/tests/promote_this_website_i18n.test.ts +33 -0
  17. package/src/tests/slug_language_code_format.test.ts +25 -25
  18. package/src/tests/slug_uniqueness.test.ts +86 -86
  19. package/src/tests/tool_validation.test.ts +15 -15
  20. package/src/tool/aspectRatio/i18n/de.ts +164 -164
  21. package/src/tool/aspectRatio/i18n/ja.ts +1 -1
  22. package/src/tool/aspectRatio/i18n/ko.ts +1 -1
  23. package/src/tool/aspectRatio/i18n/pl.ts +164 -164
  24. package/src/tool/aspectRatio/i18n/ru.ts +164 -164
  25. package/src/tool/aspectRatio/i18n/zh.ts +165 -165
  26. package/src/tool/aspectRatio/seo.astro +15 -15
  27. package/src/tool/calculadoraTiempoDatos/i18n/de.ts +143 -143
  28. package/src/tool/calculadoraTiempoDatos/i18n/ja.ts +1 -1
  29. package/src/tool/calculadoraTiempoDatos/i18n/ko.ts +1 -1
  30. package/src/tool/calculadoraTiempoDatos/i18n/pl.ts +143 -143
  31. package/src/tool/calculadoraTiempoDatos/i18n/ru.ts +144 -144
  32. package/src/tool/calculadoraTiempoDatos/i18n/sv.ts +143 -143
  33. package/src/tool/calculadoraTiempoDatos/i18n/zh.ts +144 -144
  34. package/src/tool/calculadoraTiempoDatos/seo.astro +15 -15
  35. package/src/tool/colorConverter/i18n/de.ts +120 -120
  36. package/src/tool/colorConverter/i18n/ja.ts +1 -1
  37. package/src/tool/colorConverter/i18n/ko.ts +1 -1
  38. package/src/tool/colorConverter/i18n/ru.ts +120 -120
  39. package/src/tool/colorConverter/i18n/zh.ts +1 -1
  40. package/src/tool/colorConverter/seo.astro +15 -15
  41. package/src/tool/conversorExcelCsvHtml/i18n/ja.ts +1 -1
  42. package/src/tool/conversorExcelCsvHtml/i18n/ko.ts +1 -1
  43. package/src/tool/conversorExcelCsvHtml/i18n/pl.ts +124 -124
  44. package/src/tool/conversorExcelCsvHtml/i18n/ru.ts +124 -124
  45. package/src/tool/conversorExcelCsvHtml/i18n/zh.ts +125 -125
  46. package/src/tool/conversorExcelCsvHtml/seo.astro +15 -15
  47. package/src/tool/cronGenerator/i18n/ja.ts +1 -1
  48. package/src/tool/cronGenerator/i18n/ko.ts +1 -1
  49. package/src/tool/cronGenerator/i18n/zh.ts +183 -183
  50. package/src/tool/cronGenerator/seo.astro +15 -15
  51. package/src/tool/cssSpecificityCalculator/i18n/ja.ts +1 -1
  52. package/src/tool/cssSpecificityCalculator/i18n/ko.ts +1 -1
  53. package/src/tool/cssSpecificityCalculator/i18n/zh.ts +1 -1
  54. package/src/tool/cssSpecificityCalculator/seo.astro +15 -15
  55. package/src/tool/cssToInlineConverter/i18n/ja.ts +1 -1
  56. package/src/tool/cssToInlineConverter/i18n/ko.ts +1 -1
  57. package/src/tool/cssToInlineConverter/i18n/zh.ts +1 -1
  58. package/src/tool/cssToInlineConverter/seo.astro +15 -15
  59. package/src/tool/duplicateCssRemover/i18n/ja.ts +1 -1
  60. package/src/tool/duplicateCssRemover/i18n/ko.ts +1 -1
  61. package/src/tool/duplicateCssRemover/i18n/zh.ts +1 -1
  62. package/src/tool/duplicateCssRemover/seo.astro +15 -15
  63. package/src/tool/generadorSecurityTxt/i18n/ja.ts +1 -1
  64. package/src/tool/generadorSecurityTxt/i18n/ko.ts +1 -1
  65. package/src/tool/generadorSecurityTxt/i18n/zh.ts +1 -1
  66. package/src/tool/generadorSecurityTxt/seo.astro +15 -15
  67. package/src/tool/hashGenerator/i18n/ja.ts +1 -1
  68. package/src/tool/hashGenerator/i18n/ko.ts +1 -1
  69. package/src/tool/hashGenerator/seo.astro +15 -15
  70. package/src/tool/inspectorCertificadosSsl/i18n/ja.ts +1 -1
  71. package/src/tool/inspectorCertificadosSsl/i18n/ko.ts +1 -1
  72. package/src/tool/inspectorCertificadosSsl/i18n/zh.ts +1 -1
  73. package/src/tool/inspectorCertificadosSsl/seo.astro +15 -15
  74. package/src/tool/jsonFormatter/seo.astro +15 -15
  75. package/src/tool/jwtDecoder/i18n/es.ts +248 -248
  76. package/src/tool/jwtDecoder/i18n/fr.ts +248 -248
  77. package/src/tool/jwtDecoder/i18n/tr.ts +248 -248
  78. package/src/tool/keycode/seo.astro +15 -15
  79. package/src/tool/llmCostCalculator/i18n/fr.ts +149 -149
  80. package/src/tool/llmCostCalculator/i18n/ja.ts +1 -1
  81. package/src/tool/llmCostCalculator/i18n/ko.ts +1 -1
  82. package/src/tool/llmCostCalculator/seo.astro +15 -15
  83. package/src/tool/mobileMockupGenerator/i18n/ja.ts +1 -1
  84. package/src/tool/mobileMockupGenerator/i18n/ko.ts +1 -1
  85. package/src/tool/mobileMockupGenerator/i18n/zh.ts +1 -1
  86. package/src/tool/mobileMockupGenerator/seo.astro +15 -15
  87. package/src/tool/musicalTypography/i18n/ja.ts +1 -1
  88. package/src/tool/musicalTypography/i18n/ko.ts +1 -1
  89. package/src/tool/musicalTypography/i18n/zh.ts +1 -1
  90. package/src/tool/musicalTypography/seo.astro +15 -15
  91. package/src/tool/placeholderGenerator/i18n/ja.ts +1 -1
  92. package/src/tool/placeholderGenerator/i18n/ko.ts +1 -1
  93. package/src/tool/placeholderGenerator/i18n/zh.ts +1 -1
  94. package/src/tool/placeholderGenerator/seo.astro +15 -15
  95. package/src/tool/promoteThisWebsite/entry.ts +13 -0
  96. package/src/tool/promoteThisWebsite/i18n/de.ts +95 -0
  97. package/src/tool/promoteThisWebsite/i18n/fr.ts +95 -0
  98. package/src/tool/promoteThisWebsite/i18n/id.ts +95 -0
  99. package/src/tool/promoteThisWebsite/i18n/it.ts +95 -0
  100. package/src/tool/promoteThisWebsite/i18n/ja.ts +95 -0
  101. package/src/tool/promoteThisWebsite/i18n/ko.ts +95 -0
  102. package/src/tool/promoteThisWebsite/i18n/nl.ts +95 -0
  103. package/src/tool/promoteThisWebsite/i18n/pl.ts +95 -0
  104. package/src/tool/promoteThisWebsite/i18n/pt.ts +95 -0
  105. package/src/tool/promoteThisWebsite/i18n/ru.ts +95 -0
  106. package/src/tool/promoteThisWebsite/i18n/sv.ts +95 -0
  107. package/src/tool/promoteThisWebsite/i18n/tr.ts +95 -0
  108. package/src/tool/promoteThisWebsite/i18n/zh.ts +95 -0
  109. package/src/tool/promptLibrary/i18n/ja.ts +1 -1
  110. package/src/tool/promptLibrary/i18n/ko.ts +1 -1
  111. package/src/tool/promptLibrary/i18n/zh.ts +1 -1
  112. package/src/tool/promptLibrary/seo.astro +15 -15
  113. package/src/tool/readabilityCalculator/i18n/ja.ts +1 -1
  114. package/src/tool/readabilityCalculator/i18n/ko.ts +1 -1
  115. package/src/tool/readabilityCalculator/i18n/zh.ts +1 -1
  116. package/src/tool/readabilityCalculator/seo.astro +15 -15
  117. package/src/tool/serpPixelSimulator/entry.ts +30 -30
  118. package/src/tool/serpPixelSimulator/i18n/en.ts +211 -211
  119. package/src/tool/serpPixelSimulator/i18n/fr.ts +214 -214
  120. package/src/tool/serpPixelSimulator/i18n/ru.ts +214 -214
  121. package/src/tool/serpPixelSimulator/logic.ts +18 -18
  122. package/src/tool/serpPixelSimulator/serp-pixel-simulator.css +195 -195
  123. package/src/tool/svgSanitizer/i18n/ja.ts +1 -1
  124. package/src/tool/svgSanitizer/i18n/ko.ts +1 -1
  125. package/src/tool/svgSanitizer/seo.astro +15 -15
  126. package/src/tool/svgToCss/i18n/ja.ts +1 -1
  127. package/src/tool/svgToCss/i18n/ko.ts +1 -1
  128. package/src/tool/svgToCss/seo.astro +15 -15
  129. package/src/tool/urlCleaner/i18n/ja.ts +1 -1
  130. package/src/tool/urlCleaner/i18n/ko.ts +1 -1
  131. package/src/tool/urlCleaner/i18n/zh.ts +1 -1
  132. package/src/tool/urlCleaner/seo.astro +15 -15
  133. package/src/tool/urlEncoderDecoder/i18n/es.ts +146 -146
  134. package/src/tool/urlEncoderDecoder/i18n/ko.ts +1 -1
  135. package/src/tool/urlEncoderDecoder/seo.astro +15 -15
  136. package/src/tool/utmGenerator/i18n/ja.ts +1 -1
  137. package/src/tool/utmGenerator/i18n/ko.ts +1 -1
  138. package/src/tool/utmGenerator/i18n/zh.ts +1 -1
  139. package/src/tool/utmGenerator/seo.astro +15 -15
  140. package/src/types.ts +1 -1
@@ -1,113 +1,113 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { ALL_TOOLS } from '../tools';
3
-
4
- const INVERTED_PUNCTUATION_LOCALES = {
5
- es: {
6
- language: 'Spanish',
7
- questionStart: '¿',
8
- questionEnd: '?',
9
- exclamationStart: '¡',
10
- exclamationEnd: '!',
11
- },
12
- } as const;
13
-
14
- type InvertedPunctuationLocale = keyof typeof INVERTED_PUNCTUATION_LOCALES;
15
-
16
- const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
17
-
18
- function collectStrings(value: unknown): string[] {
19
- if (typeof value === 'string') return [value];
20
- if (!value || typeof value !== 'object') return [];
21
- if (Array.isArray(value)) return value.flatMap(collectStrings);
22
- return Object.values(value).flatMap(collectStrings);
23
- }
24
-
25
- function translatableStrings(content: Record<string, unknown>): string[] {
26
- return TRANSLATABLE_KEYS.flatMap((key) => collectStrings(content[key]));
27
- }
28
-
29
- function sentenceStart(text: string, endIndex: number): string {
30
- const beforeMark = text.slice(0, endIndex).trimEnd();
31
-
32
- const boundaries = [
33
- ...[...beforeMark.matchAll(/\.(?=\s|$)/g)].map((m) => m.index ?? 0),
34
- ...[...beforeMark.matchAll(/:(?=\s|$)/g)].map((m) => m.index ?? 0),
35
- ...[...beforeMark.matchAll(/;(?=\s|$)/g)].map((m) => m.index ?? 0),
36
- ];
37
-
38
- const lastBoundary = boundaries.length > 0
39
- ? Math.max(...boundaries)
40
- : -1;
41
-
42
- const nlBoundary = beforeMark.lastIndexOf('\n');
43
- const boundary = Math.max(lastBoundary, nlBoundary);
44
-
45
- return beforeMark.slice(boundary + 1).trimStart();
46
- }
47
-
48
- function isCodeContext(text: string, index: number, mark: string): boolean {
49
- if (mark === '!') {
50
- const after = text.slice(index + 1, index + 10);
51
- return /^important\b/.test(after);
52
- }
53
- if (mark === '?') {
54
- const before = text.slice(Math.max(0, index - 2), index);
55
- return /[,#\/&+=]/.test(before) || / y $/.test(text.slice(0, index));
56
- }
57
- return false;
58
- }
59
-
60
- function findMissingInvertedMarks(
61
- text: string,
62
- startMark: string,
63
- endMark: string,
64
- _toolId: string,
65
- ): string[] {
66
- const cleaned = text.replace(/<code>.*?<\/code>/g, '');
67
-
68
- return [...cleaned.matchAll(new RegExp(`\\${endMark}`, 'g'))]
69
- .filter((match) => !isCodeContext(cleaned, match.index ?? 0, endMark))
70
- .map((match) => sentenceStart(cleaned, match.index ?? 0))
71
- .filter((segment) => {
72
- if (segment.length === 0) return false;
73
- if (segment.includes(startMark)) return false;
74
- if (endMark === '?' && /:\/\//.test(segment)) return false;
75
- return true;
76
- })
77
- .map((segment) => `${segment}${endMark}`);
78
- }
79
-
80
- describe('Inverted punctuation validation', () => {
81
- ALL_TOOLS.forEach((tool) => {
82
- describe(`Tool: ${tool.entry.id}`, () => {
83
- Object.keys(INVERTED_PUNCTUATION_LOCALES).forEach((locale) => {
84
- it(`${locale} uses opening punctuation marks for questions and exclamations`, async () => {
85
- const typedLocale = locale as InvertedPunctuationLocale;
86
- const loader = tool.entry.i18n[typedLocale];
87
- if (!loader) return;
88
-
89
- const rule = INVERTED_PUNCTUATION_LOCALES[typedLocale];
90
- const content = await loader();
91
- const strings = translatableStrings(content as Record<string, unknown>);
92
- const toolId = tool.entry.id;
93
- const missingQuestions = strings.flatMap((text) =>
94
- findMissingInvertedMarks(text, rule.questionStart, rule.questionEnd, toolId)
95
- );
96
- const missingExclamations = strings.flatMap((text) =>
97
- findMissingInvertedMarks(text, rule.exclamationStart, rule.exclamationEnd, toolId)
98
- );
99
- const failures = [...missingQuestions, ...missingExclamations];
100
-
101
- expect(
102
- failures,
103
- [
104
- `Missing opening punctuation marks in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
105
- `Questions must use ${rule.questionStart}...${rule.questionEnd} and exclamations must use ${rule.exclamationStart}...${rule.exclamationEnd}.`,
106
- `Examples: ${failures.slice(0, 5).join(' | ')}`,
107
- ].join(' '),
108
- ).toHaveLength(0);
109
- });
110
- });
111
- });
112
- });
113
- });
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+
4
+ const INVERTED_PUNCTUATION_LOCALES = {
5
+ es: {
6
+ language: 'Spanish',
7
+ questionStart: '¿',
8
+ questionEnd: '?',
9
+ exclamationStart: '¡',
10
+ exclamationEnd: '!',
11
+ },
12
+ } as const;
13
+
14
+ type InvertedPunctuationLocale = keyof typeof INVERTED_PUNCTUATION_LOCALES;
15
+
16
+ const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
17
+
18
+ function collectStrings(value: unknown): string[] {
19
+ if (typeof value === 'string') return [value];
20
+ if (!value || typeof value !== 'object') return [];
21
+ if (Array.isArray(value)) return value.flatMap(collectStrings);
22
+ return Object.values(value).flatMap(collectStrings);
23
+ }
24
+
25
+ function translatableStrings(content: Record<string, unknown>): string[] {
26
+ return TRANSLATABLE_KEYS.flatMap((key) => collectStrings(content[key]));
27
+ }
28
+
29
+ function sentenceStart(text: string, endIndex: number): string {
30
+ const beforeMark = text.slice(0, endIndex).trimEnd();
31
+
32
+ const boundaries = [
33
+ ...[...beforeMark.matchAll(/\.(?=\s|$)/g)].map((m) => m.index ?? 0),
34
+ ...[...beforeMark.matchAll(/:(?=\s|$)/g)].map((m) => m.index ?? 0),
35
+ ...[...beforeMark.matchAll(/;(?=\s|$)/g)].map((m) => m.index ?? 0),
36
+ ];
37
+
38
+ const lastBoundary = boundaries.length > 0
39
+ ? Math.max(...boundaries)
40
+ : -1;
41
+
42
+ const nlBoundary = beforeMark.lastIndexOf('\n');
43
+ const boundary = Math.max(lastBoundary, nlBoundary);
44
+
45
+ return beforeMark.slice(boundary + 1).trimStart();
46
+ }
47
+
48
+ function isCodeContext(text: string, index: number, mark: string): boolean {
49
+ if (mark === '!') {
50
+ const after = text.slice(index + 1, index + 10);
51
+ return /^important\b/.test(after);
52
+ }
53
+ if (mark === '?') {
54
+ const before = text.slice(Math.max(0, index - 2), index);
55
+ return /[,#\/&+=]/.test(before) || / y $/.test(text.slice(0, index));
56
+ }
57
+ return false;
58
+ }
59
+
60
+ function findMissingInvertedMarks(
61
+ text: string,
62
+ startMark: string,
63
+ endMark: string,
64
+ _toolId: string,
65
+ ): string[] {
66
+ const cleaned = text.replace(/<code>.*?<\/code>/g, '');
67
+
68
+ return [...cleaned.matchAll(new RegExp(`\\${endMark}`, 'g'))]
69
+ .filter((match) => !isCodeContext(cleaned, match.index ?? 0, endMark))
70
+ .map((match) => sentenceStart(cleaned, match.index ?? 0))
71
+ .filter((segment) => {
72
+ if (segment.length === 0) return false;
73
+ if (segment.includes(startMark)) return false;
74
+ if (endMark === '?' && /:\/\//.test(segment)) return false;
75
+ return true;
76
+ })
77
+ .map((segment) => `${segment}${endMark}`);
78
+ }
79
+
80
+ describe('Inverted punctuation validation', () => {
81
+ ALL_TOOLS.forEach((tool) => {
82
+ describe(`Tool: ${tool.entry.id}`, () => {
83
+ Object.keys(INVERTED_PUNCTUATION_LOCALES).forEach((locale) => {
84
+ it(`${locale} uses opening punctuation marks for questions and exclamations`, async () => {
85
+ const typedLocale = locale as InvertedPunctuationLocale;
86
+ const loader = tool.entry.i18n[typedLocale];
87
+ if (!loader) return;
88
+
89
+ const rule = INVERTED_PUNCTUATION_LOCALES[typedLocale];
90
+ const content = await loader();
91
+ const strings = translatableStrings(content as Record<string, unknown>);
92
+ const toolId = tool.entry.id;
93
+ const missingQuestions = strings.flatMap((text) =>
94
+ findMissingInvertedMarks(text, rule.questionStart, rule.questionEnd, toolId)
95
+ );
96
+ const missingExclamations = strings.flatMap((text) =>
97
+ findMissingInvertedMarks(text, rule.exclamationStart, rule.exclamationEnd, toolId)
98
+ );
99
+ const failures = [...missingQuestions, ...missingExclamations];
100
+
101
+ expect(
102
+ failures,
103
+ [
104
+ `Missing opening punctuation marks in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
105
+ `Questions must use ${rule.questionStart}...${rule.questionEnd} and exclamations must use ${rule.exclamationStart}...${rule.exclamationEnd}.`,
106
+ `Examples: ${failures.slice(0, 5).join(' | ')}`,
107
+ ].join(' '),
108
+ ).toHaveLength(0);
109
+ });
110
+ });
111
+ });
112
+ });
113
+ });
@@ -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
+
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { promoteThisWebsite } from '../tool/promoteThisWebsite/entry';
3
+
4
+ const locales = [
5
+ 'de', 'en', 'es', 'fr', 'id', 'it', 'ja', 'ko', 'nl', 'pl', 'pt', 'ru', 'sv', 'tr', 'zh',
6
+ ];
7
+
8
+ describe('promote-this-website translations', () => {
9
+ it('provides every visible UI key in every locale', async () => {
10
+ const english = await promoteThisWebsite.i18n.en!();
11
+ const expectedKeys = Object.keys(english.ui).sort();
12
+
13
+ for (const locale of locales) {
14
+ const content = await promoteThisWebsite.i18n[locale as keyof typeof promoteThisWebsite.i18n]!();
15
+ expect(Object.keys(content.ui).sort(), `${locale} has an incomplete UI translation`).toEqual(expectedKeys);
16
+ expect(content.faq).toHaveLength(english.faq.length);
17
+ expect(content.howTo).toHaveLength(english.howTo.length);
18
+ expect(content.seo).toHaveLength(english.seo.length);
19
+ }
20
+ });
21
+
22
+ it('does not silently use English for the core translated controls', async () => {
23
+ const english = await promoteThisWebsite.i18n.en!();
24
+ const translatedControls = ['urlLabel', 'applyUrl', 'titleLabel', 'download', 'reset', 'titleStyleLabel', 'brandStyleLabel'];
25
+
26
+ for (const locale of locales.filter((locale) => locale !== 'en')) {
27
+ const content = await promoteThisWebsite.i18n[locale as keyof typeof promoteThisWebsite.i18n]!();
28
+ for (const key of translatedControls) {
29
+ expect(content.ui[key], `${locale}.${key} is still English`).not.toBe(english.ui[key]);
30
+ }
31
+ }
32
+ });
33
+ });
@@ -1,25 +1,25 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { ALL_TOOLS } from '../tools';
3
- import type { ToolLocaleContent } from '../types';
4
-
5
- describe('Slug Language Code Format Validation', () => {
6
-
7
-
8
- ALL_TOOLS.forEach((tool) => {
9
- describe(`Tool: ${tool.entry.id}`, () => {
10
- it('slug should not end with 2-letter language codes like -ja, -ru, -ko', async () => {
11
- const locales = Object.keys(tool.entry.i18n);
12
-
13
- for (const locale of locales) {
14
- const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
15
- const content = (await loader?.()) as ToolLocaleContent;
16
-
17
- expect(
18
- content.slug,
19
- `Tool "${tool.entry.id}" locale "${locale}" slug ("${content.slug}") cannot end with a 2-letter language code (e.g., -ja, -ru, -ko).`,
20
- ).not.toMatch(/-[a-z]{2}$/);
21
- }
22
- });
23
- });
24
- });
25
- });
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+ import type { ToolLocaleContent } from '../types';
4
+
5
+ describe('Slug Language Code Format Validation', () => {
6
+
7
+
8
+ ALL_TOOLS.forEach((tool) => {
9
+ describe(`Tool: ${tool.entry.id}`, () => {
10
+ it('slug should not end with 2-letter language codes like -ja, -ru, -ko', async () => {
11
+ const locales = Object.keys(tool.entry.i18n);
12
+
13
+ for (const locale of locales) {
14
+ const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
15
+ const content = (await loader?.()) as ToolLocaleContent;
16
+
17
+ expect(
18
+ content.slug,
19
+ `Tool "${tool.entry.id}" locale "${locale}" slug ("${content.slug}") cannot end with a 2-letter language code (e.g., -ja, -ru, -ko).`,
20
+ ).not.toMatch(/-[a-z]{2}$/);
21
+ }
22
+ });
23
+ });
24
+ });
25
+ });
@@ -1,87 +1,87 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { ALL_TOOLS } from '../tools';
3
- import type { ToolLocaleContent } from '../types';
4
-
5
- const sharingLocales = ['ja', 'ko', 'zh'];
6
- const toolsWithSharedSpanishSlug = ['cron-generator', 'json-formatter', 'keycode'];
7
-
8
- interface ValidateParams {
9
- toolId: string;
10
- locale: string;
11
- content: ToolLocaleContent;
12
- enSlug: string;
13
- slugs: Map<string, string>;
14
- }
15
-
16
- const validateLocaleSlug = ({
17
- toolId,
18
- locale,
19
- content,
20
- enSlug,
21
- slugs,
22
- }: ValidateParams) => {
23
- expect(
24
- content.slug,
25
- `Tool "${toolId}" locale "${locale}" has an invalid slug ("${content.slug}"). Slugs must be transliterated (only a-z, 0-9, and -).`,
26
- ).toMatch(/^[a-z0-9-]+$/);
27
-
28
- if (locale === 'en') {
29
- return;
30
- }
31
-
32
- if (sharingLocales.includes(locale)) {
33
- expect(
34
- content.slug,
35
- `Tool "${toolId}" locale "${locale}" must use the same slug as "en" ("${enSlug}").`,
36
- ).toBe(enSlug);
37
- } else if (locale === 'es' && toolsWithSharedSpanishSlug.includes(toolId)) {
38
- expect(
39
- content.slug,
40
- `Tool "${toolId}" locale "${locale}" must use the same slug as "en" ("${enSlug}").`,
41
- ).toBe(enSlug);
42
- } else {
43
- expect(
44
- content.slug,
45
- `Tool "${toolId}" locale "${locale}" has the same slug as "en" ("${enSlug}"). Cada slug tiene que estar en su propia idioma`,
46
- ).not.toBe(enSlug);
47
-
48
- if (slugs.has(content.slug)) {
49
- const previousLocale = slugs.get(content.slug);
50
- expect(
51
- false,
52
- `Tool "${toolId}" locales "${locale}" and "${previousLocale}" share the same slug ("${content.slug}"). Cada slug tiene que estar en su propia idioma`,
53
- ).toBe(true);
54
- }
55
- slugs.set(content.slug, locale);
56
- }
57
- };
58
-
59
- describe('Slug Localization and Uniqueness Validation', () => {
60
- ALL_TOOLS.forEach((tool) => {
61
- describe(`Tool: ${tool.entry.id}`, () => {
62
- it('every locale should have a unique, translated slug', async () => {
63
- const slugs = new Map<string, string>();
64
- const locales = Object.keys(tool.entry.i18n);
65
-
66
- let enSlug = '';
67
- if (locales.includes('en')) {
68
- const enLoader = tool.entry.i18n['en' as keyof typeof tool.entry.i18n];
69
- const enContent = (await enLoader?.()) as ToolLocaleContent;
70
- enSlug = enContent.slug;
71
- }
72
-
73
- for (const locale of locales) {
74
- const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
75
- const content = (await loader?.()) as ToolLocaleContent;
76
- validateLocaleSlug({
77
- toolId: tool.entry.id,
78
- locale,
79
- content,
80
- enSlug,
81
- slugs,
82
- });
83
- }
84
- });
85
- });
86
- });
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+ import type { ToolLocaleContent } from '../types';
4
+
5
+ const sharingLocales = ['ja', 'ko', 'zh'];
6
+ const toolsWithSharedSpanishSlug = ['cron-generator', 'json-formatter', 'keycode'];
7
+
8
+ interface ValidateParams {
9
+ toolId: string;
10
+ locale: string;
11
+ content: ToolLocaleContent;
12
+ enSlug: string;
13
+ slugs: Map<string, string>;
14
+ }
15
+
16
+ const validateLocaleSlug = ({
17
+ toolId,
18
+ locale,
19
+ content,
20
+ enSlug,
21
+ slugs,
22
+ }: ValidateParams) => {
23
+ expect(
24
+ content.slug,
25
+ `Tool "${toolId}" locale "${locale}" has an invalid slug ("${content.slug}"). Slugs must be transliterated (only a-z, 0-9, and -).`,
26
+ ).toMatch(/^[a-z0-9-]+$/);
27
+
28
+ if (locale === 'en') {
29
+ return;
30
+ }
31
+
32
+ if (sharingLocales.includes(locale)) {
33
+ expect(
34
+ content.slug,
35
+ `Tool "${toolId}" locale "${locale}" must use the same slug as "en" ("${enSlug}").`,
36
+ ).toBe(enSlug);
37
+ } else if (locale === 'es' && toolsWithSharedSpanishSlug.includes(toolId)) {
38
+ expect(
39
+ content.slug,
40
+ `Tool "${toolId}" locale "${locale}" must use the same slug as "en" ("${enSlug}").`,
41
+ ).toBe(enSlug);
42
+ } else {
43
+ expect(
44
+ content.slug,
45
+ `Tool "${toolId}" locale "${locale}" has the same slug as "en" ("${enSlug}"). Cada slug tiene que estar en su propia idioma`,
46
+ ).not.toBe(enSlug);
47
+
48
+ if (slugs.has(content.slug)) {
49
+ const previousLocale = slugs.get(content.slug);
50
+ expect(
51
+ false,
52
+ `Tool "${toolId}" locales "${locale}" and "${previousLocale}" share the same slug ("${content.slug}"). Cada slug tiene que estar en su propia idioma`,
53
+ ).toBe(true);
54
+ }
55
+ slugs.set(content.slug, locale);
56
+ }
57
+ };
58
+
59
+ describe('Slug Localization and Uniqueness Validation', () => {
60
+ ALL_TOOLS.forEach((tool) => {
61
+ describe(`Tool: ${tool.entry.id}`, () => {
62
+ it('every locale should have a unique, translated slug', async () => {
63
+ const slugs = new Map<string, string>();
64
+ const locales = Object.keys(tool.entry.i18n);
65
+
66
+ let enSlug = '';
67
+ if (locales.includes('en')) {
68
+ const enLoader = tool.entry.i18n['en' as keyof typeof tool.entry.i18n];
69
+ const enContent = (await enLoader?.()) as ToolLocaleContent;
70
+ enSlug = enContent.slug;
71
+ }
72
+
73
+ for (const locale of locales) {
74
+ const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
75
+ const content = (await loader?.()) as ToolLocaleContent;
76
+ validateLocaleSlug({
77
+ toolId: tool.entry.id,
78
+ locale,
79
+ content,
80
+ enSlug,
81
+ slugs,
82
+ });
83
+ }
84
+ });
85
+ });
86
+ });
87
87
  });
@@ -1,17 +1,17 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { ALL_TOOLS } from '../tools';
3
- import { developerCategory } from '../data';
4
-
5
- describe('Tool Validation Suite', () => {
6
- describe('Library Registration', () => {
7
- it('should have 28 tools in ALL_TOOLS', () => {
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+ import { developerCategory } from '../data';
4
+
5
+ describe('Tool Validation Suite', () => {
6
+ describe('Library Registration', () => {
7
+ it('should have 28 tools in ALL_TOOLS', () => {
8
8
  expect(ALL_TOOLS.length).toBe(29);
9
9
  });
10
-
11
- it('developerCategory should be defined', () => {
12
- expect(developerCategory).toBeDefined();
13
- expect(developerCategory.i18n).toBeDefined();
14
- });
15
- });
16
- });
17
-
10
+
11
+ it('developerCategory should be defined', () => {
12
+ expect(developerCategory).toBeDefined();
13
+ expect(developerCategory.i18n).toBeDefined();
14
+ });
15
+ });
16
+ });
17
+