@jjlmoya/utils-games-development 1.66.0 → 1.67.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 (71) hide show
  1. package/package.json +67 -67
  2. package/scripts/postinstall.mjs +27 -27
  3. package/src/category/GamesCategorySEO.astro +19 -19
  4. package/src/category/i18n/de.ts +15 -15
  5. package/src/category/i18n/en.ts +15 -15
  6. package/src/category/i18n/es.ts +15 -15
  7. package/src/category/i18n/fr.ts +15 -15
  8. package/src/category/i18n/id.ts +10 -10
  9. package/src/category/i18n/it.ts +10 -10
  10. package/src/category/i18n/ja.ts +10 -10
  11. package/src/category/i18n/ko.ts +10 -10
  12. package/src/category/i18n/nl.ts +10 -10
  13. package/src/category/i18n/pl.ts +10 -10
  14. package/src/category/i18n/pt.ts +10 -10
  15. package/src/category/i18n/ru.ts +10 -10
  16. package/src/category/i18n/sv.ts +10 -10
  17. package/src/category/i18n/tr.ts +10 -10
  18. package/src/category/i18n/zh.ts +10 -10
  19. package/src/category/seo.astro +15 -15
  20. package/src/components/PreviewNavSidebar.astro +116 -116
  21. package/src/components/PreviewToolbar.astro +143 -143
  22. package/src/data.ts +11 -11
  23. package/src/env.d.ts +5 -5
  24. package/src/index.ts +20 -20
  25. package/src/layouts/PreviewLayout.astro +122 -122
  26. package/src/pages/[locale]/[slug].astro +165 -165
  27. package/src/pages/[locale].astro +250 -250
  28. package/src/pages/index.astro +4 -4
  29. package/src/tests/category_seo_quality.test.ts +74 -0
  30. package/src/tests/diacritics_density.test.ts +118 -118
  31. package/src/tests/faq_count.test.ts +19 -19
  32. package/src/tests/i18n_coverage.test.ts +36 -36
  33. package/src/tests/inverted_punctuation.test.ts +84 -84
  34. package/src/tests/mocks/astro_mock.js +2 -2
  35. package/src/tests/no_en_dash.test.ts +70 -70
  36. package/src/tests/no_h1_in_components.test.ts +48 -48
  37. package/src/tests/pagespeed_best_practices.test.ts +198 -198
  38. package/src/tests/qa-test-helpers.ts +32 -32
  39. package/src/tests/qa_bibliography_links.test.ts +41 -41
  40. package/src/tests/qa_claim_evidence.test.ts +69 -69
  41. package/src/tests/qa_logic_reference_coverage.test.ts +46 -46
  42. package/src/tests/qa_runtime_i18n.test.ts +100 -100
  43. package/src/tests/schemas_fulfillment.test.ts +23 -23
  44. package/src/tests/script_density.test.ts +94 -94
  45. package/src/tests/seo_length.test.ts +23 -23
  46. package/src/tests/slug_language_code_format.test.ts +23 -23
  47. package/src/tests/slug_uniqueness.test.ts +80 -80
  48. package/src/tests/title_quality.test.ts +56 -56
  49. package/src/tool/audioLoopPointFinder/audio-loop-point-finder.css +322 -322
  50. package/src/tool/audioLoopPointFinder/client.ts +262 -262
  51. package/src/tool/hitboxHurtboxAnimator/hitbox-hurtbox-animator.css +614 -614
  52. package/src/tool/saveFileEditor/component.astro +351 -351
  53. package/src/tool/saveFileEditor/game-save-file-editor.css +250 -250
  54. package/src/tool/spriteSheetPacker/app-client.ts +248 -248
  55. package/src/tool/spriteSheetPacker/bibliography.ts +12 -12
  56. package/src/tool/spriteSheetPacker/component.astro +229 -229
  57. package/src/tool/spriteSheetPacker/dropzone-client.ts +55 -55
  58. package/src/tool/spriteSheetPacker/entry.ts +28 -28
  59. package/src/tool/spriteSheetPacker/exporter.ts +116 -116
  60. package/src/tool/spriteSheetPacker/extractor-client.ts +127 -127
  61. package/src/tool/spriteSheetPacker/flipbook-client.ts +60 -60
  62. package/src/tool/spriteSheetPacker/logic.test.ts +155 -155
  63. package/src/tool/spriteSheetPacker/logic.ts +32 -32
  64. package/src/tool/spriteSheetPacker/packer-core.ts +121 -121
  65. package/src/tool/spriteSheetPacker/packer-ui.ts +86 -86
  66. package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +578 -578
  67. package/src/tool/spriteSheetPacker/types.ts +89 -89
  68. package/src/tool/spriteSheetPacker/ui.ts +42 -42
  69. package/src/tool/steamCapsuleGenerator/index.ts +9 -9
  70. package/src/tool/steamCapsuleGenerator/validation.ts +14 -14
  71. package/src/types.ts +72 -72
@@ -1,94 +1,94 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { ALL_TOOLS } from '../tools';
3
-
4
- type ScriptLocale = keyof typeof SCRIPT_RULES;
5
-
6
- const SCRIPT_RULES = {
7
- ja: {
8
- language: 'Japanese',
9
- scriptName: 'kana/kanji',
10
- scriptCharacters: /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/gu,
11
- minScriptRatio: 0.45,
12
- },
13
- ko: {
14
- language: 'Korean',
15
- scriptName: 'hangul',
16
- scriptCharacters: /\p{Script=Hangul}/gu,
17
- minScriptRatio: 0.55,
18
- },
19
- ru: {
20
- language: 'Russian',
21
- scriptName: 'cyrillic',
22
- scriptCharacters: /\p{Script=Cyrillic}/gu,
23
- minScriptRatio: 0.65,
24
- },
25
- zh: {
26
- language: 'Chinese',
27
- scriptName: 'han',
28
- scriptCharacters: /\p{Script=Han}/gu,
29
- minScriptRatio: 0.45,
30
- },
31
- } as const;
32
-
33
- const LETTERS = /\p{L}/gu;
34
- const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
35
-
36
- function collectStrings(value: unknown): string[] {
37
- if (typeof value === 'string') return [value];
38
- if (!value || typeof value !== 'object') return [];
39
- if (Array.isArray(value)) return value.flatMap(collectStrings);
40
- return Object.values(value).flatMap(collectStrings);
41
- }
42
-
43
- function normalizeText(value: unknown): string {
44
- return collectStrings(value).join(' ').normalize('NFC');
45
- }
46
-
47
- function translatableContent(content: Record<string, unknown>) {
48
- return TRANSLATABLE_KEYS.map((key) => content[key]);
49
- }
50
-
51
- function letterCount(text: string): number {
52
- return text.match(LETTERS)?.length ?? 0;
53
- }
54
-
55
- function scriptCount(text: string, locale: ScriptLocale): number {
56
- return text.match(SCRIPT_RULES[locale].scriptCharacters)?.length ?? 0;
57
- }
58
-
59
- function scriptRatio(text: string, locale: ScriptLocale): number {
60
- const letters = letterCount(text);
61
- if (letters === 0) return 0;
62
- return scriptCount(text, locale) / letters;
63
- }
64
-
65
- describe('Native script density validation', () => {
66
- ALL_TOOLS.forEach((tool) => {
67
- describe(`Tool: ${tool.entry.id}`, () => {
68
- Object.keys(SCRIPT_RULES).forEach((locale) => {
69
- it(`${locale} keeps most translated text in its native script`, async () => {
70
- const typedLocale = locale as ScriptLocale;
71
- const loader = tool.entry.i18n[typedLocale];
72
- if (!loader) return;
73
-
74
- const content = await loader();
75
- const rule = SCRIPT_RULES[typedLocale];
76
- const text = normalizeText(translatableContent(content as unknown as Record<string, unknown>));
77
- const letters = letterCount(text);
78
- const matches = scriptCount(text, typedLocale);
79
- const ratio = scriptRatio(text, typedLocale);
80
-
81
- expect(
82
- ratio,
83
- [
84
- `Possible broken translation detected in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
85
- `The text has ${matches} ${rule.scriptName} characters out of ${letters} analyzed letters (${(ratio * 100).toFixed(1)}%).`,
86
- `Most translatable content should be written in ${rule.scriptName} script.`,
87
- 'Non-translatable fields such as slug, bibliography, and schemas are ignored to avoid false positives.',
88
- ].join(' '),
89
- ).toBeGreaterThanOrEqual(rule.minScriptRatio);
90
- });
91
- });
92
- });
93
- });
94
- });
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+
4
+ type ScriptLocale = keyof typeof SCRIPT_RULES;
5
+
6
+ const SCRIPT_RULES = {
7
+ ja: {
8
+ language: 'Japanese',
9
+ scriptName: 'kana/kanji',
10
+ scriptCharacters: /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/gu,
11
+ minScriptRatio: 0.45,
12
+ },
13
+ ko: {
14
+ language: 'Korean',
15
+ scriptName: 'hangul',
16
+ scriptCharacters: /\p{Script=Hangul}/gu,
17
+ minScriptRatio: 0.55,
18
+ },
19
+ ru: {
20
+ language: 'Russian',
21
+ scriptName: 'cyrillic',
22
+ scriptCharacters: /\p{Script=Cyrillic}/gu,
23
+ minScriptRatio: 0.65,
24
+ },
25
+ zh: {
26
+ language: 'Chinese',
27
+ scriptName: 'han',
28
+ scriptCharacters: /\p{Script=Han}/gu,
29
+ minScriptRatio: 0.45,
30
+ },
31
+ } as const;
32
+
33
+ const LETTERS = /\p{L}/gu;
34
+ const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
35
+
36
+ function collectStrings(value: unknown): string[] {
37
+ if (typeof value === 'string') return [value];
38
+ if (!value || typeof value !== 'object') return [];
39
+ if (Array.isArray(value)) return value.flatMap(collectStrings);
40
+ return Object.values(value).flatMap(collectStrings);
41
+ }
42
+
43
+ function normalizeText(value: unknown): string {
44
+ return collectStrings(value).join(' ').normalize('NFC');
45
+ }
46
+
47
+ function translatableContent(content: Record<string, unknown>) {
48
+ return TRANSLATABLE_KEYS.map((key) => content[key]);
49
+ }
50
+
51
+ function letterCount(text: string): number {
52
+ return text.match(LETTERS)?.length ?? 0;
53
+ }
54
+
55
+ function scriptCount(text: string, locale: ScriptLocale): number {
56
+ return text.match(SCRIPT_RULES[locale].scriptCharacters)?.length ?? 0;
57
+ }
58
+
59
+ function scriptRatio(text: string, locale: ScriptLocale): number {
60
+ const letters = letterCount(text);
61
+ if (letters === 0) return 0;
62
+ return scriptCount(text, locale) / letters;
63
+ }
64
+
65
+ describe('Native script density validation', () => {
66
+ ALL_TOOLS.forEach((tool) => {
67
+ describe(`Tool: ${tool.entry.id}`, () => {
68
+ Object.keys(SCRIPT_RULES).forEach((locale) => {
69
+ it(`${locale} keeps most translated text in its native script`, async () => {
70
+ const typedLocale = locale as ScriptLocale;
71
+ const loader = tool.entry.i18n[typedLocale];
72
+ if (!loader) return;
73
+
74
+ const content = await loader();
75
+ const rule = SCRIPT_RULES[typedLocale];
76
+ const text = normalizeText(translatableContent(content as unknown as Record<string, unknown>));
77
+ const letters = letterCount(text);
78
+ const matches = scriptCount(text, typedLocale);
79
+ const ratio = scriptRatio(text, typedLocale);
80
+
81
+ expect(
82
+ ratio,
83
+ [
84
+ `Possible broken translation detected in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
85
+ `The text has ${matches} ${rule.scriptName} characters out of ${letters} analyzed letters (${(ratio * 100).toFixed(1)}%).`,
86
+ `Most translatable content should be written in ${rule.scriptName} script.`,
87
+ 'Non-translatable fields such as slug, bibliography, and schemas are ignored to avoid false positives.',
88
+ ].join(' '),
89
+ ).toBeGreaterThanOrEqual(rule.minScriptRatio);
90
+ });
91
+ });
92
+ });
93
+ });
94
+ });
@@ -1,23 +1,23 @@
1
- import { describe, it, expect } from 'vitest';
2
- import * as DATA from '../data';
3
-
4
- const ENTRIES = [
5
- { id: 'gamesCategory', i18n: DATA.gamesCategory.i18n },
6
- ];
7
-
8
- describe('SEO Content Length Validation', () => {
9
- ENTRIES.forEach((entry) => {
10
- describe(`Tool: ${entry.id}`, () => {
11
- Object.keys(entry.i18n).forEach((locale) => {
12
- it(`${locale}: SEO section should exist`, async () => {
13
- const loader = (entry.i18n as Record<string, () => Promise<{ seo?: unknown[] }>>)[locale];
14
- if (!loader) return;
15
- const content = await loader();
16
- if (!content.seo) return;
17
- expect(Array.isArray(content.seo)).toBe(true);
18
- });
19
- });
20
- });
21
- });
22
- });
23
-
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as DATA from '../data';
3
+
4
+ const ENTRIES = [
5
+ { id: 'gamesCategory', i18n: DATA.gamesCategory.i18n },
6
+ ];
7
+
8
+ describe('SEO Content Length Validation', () => {
9
+ ENTRIES.forEach((entry) => {
10
+ describe(`Tool: ${entry.id}`, () => {
11
+ Object.keys(entry.i18n).forEach((locale) => {
12
+ it(`${locale}: SEO section should exist`, async () => {
13
+ const loader = (entry.i18n as Record<string, () => Promise<{ seo?: unknown[] }>>)[locale];
14
+ if (!loader) return;
15
+ const content = await loader();
16
+ if (!content.seo) return;
17
+ expect(Array.isArray(content.seo)).toBe(true);
18
+ });
19
+ });
20
+ });
21
+ });
22
+ });
23
+
@@ -1,23 +1,23 @@
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
- ALL_TOOLS.forEach((tool) => {
7
- describe(`Tool: ${tool.entry.id}`, () => {
8
- it('slug should not end with 2-letter language codes like -ja, -ru, -ko', async () => {
9
- const locales = Object.keys(tool.entry.i18n);
10
-
11
- for (const locale of locales) {
12
- const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
13
- const content = (await loader?.()) as ToolLocaleContent;
14
-
15
- expect(
16
- content.slug,
17
- `Tool "${tool.entry.id}" locale "${locale}" slug ("${content.slug}") cannot end with a 2-letter language code (e.g., -ja, -ru, -ko).`,
18
- ).not.toMatch(/-[a-z]{2}$/);
19
- }
20
- });
21
- });
22
- });
23
- });
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
+ ALL_TOOLS.forEach((tool) => {
7
+ describe(`Tool: ${tool.entry.id}`, () => {
8
+ it('slug should not end with 2-letter language codes like -ja, -ru, -ko', async () => {
9
+ const locales = Object.keys(tool.entry.i18n);
10
+
11
+ for (const locale of locales) {
12
+ const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
13
+ const content = (await loader?.()) as ToolLocaleContent;
14
+
15
+ expect(
16
+ content.slug,
17
+ `Tool "${tool.entry.id}" locale "${locale}" slug ("${content.slug}") cannot end with a 2-letter language code (e.g., -ja, -ru, -ko).`,
18
+ ).not.toMatch(/-[a-z]{2}$/);
19
+ }
20
+ });
21
+ });
22
+ });
23
+ });
@@ -1,81 +1,81 @@
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
-
7
- interface ValidateParams {
8
- toolId: string;
9
- locale: string;
10
- content: ToolLocaleContent;
11
- enSlug: string;
12
- slugs: Map<string, string>;
13
- }
14
-
15
- const validateLocaleSlug = ({
16
- toolId,
17
- locale,
18
- content,
19
- enSlug,
20
- slugs,
21
- }: ValidateParams) => {
22
- expect(
23
- content.slug,
24
- `Tool "${toolId}" locale "${locale}" has an invalid slug ("${content.slug}"). Slugs must be transliterated (only a-z, 0-9, and -).`,
25
- ).toMatch(/^[a-z0-9-]+$/);
26
-
27
- if (locale === 'en') {
28
- return;
29
- }
30
-
31
- if (sharingLocales.includes(locale)) {
32
- expect(
33
- content.slug,
34
- `Tool "${toolId}" locale "${locale}" must use the same slug as "en" ("${enSlug}").`,
35
- ).toBe(enSlug);
36
- } else {
37
- expect(
38
- content.slug,
39
- `Tool "${toolId}" locale "${locale}" has the same slug as "en" ("${enSlug}"). Cada slug tiene que estar en su propia idioma`,
40
- ).not.toBe(enSlug);
41
-
42
- if (slugs.has(content.slug)) {
43
- const previousLocale = slugs.get(content.slug);
44
- expect(
45
- false,
46
- `Tool "${toolId}" locales "${locale}" and "${previousLocale}" share the same slug ("${content.slug}"). Cada slug tiene que estar en su propia idioma`,
47
- ).toBe(true);
48
- }
49
- slugs.set(content.slug, locale);
50
- }
51
- };
52
-
53
- describe('Slug Localization and Uniqueness Validation', () => {
54
- ALL_TOOLS.forEach((tool) => {
55
- describe(`Tool: ${tool.entry.id}`, () => {
56
- it('every locale should have a unique, translated slug', async () => {
57
- const slugs = new Map<string, string>();
58
- const locales = Object.keys(tool.entry.i18n);
59
-
60
- let enSlug = '';
61
- if (locales.includes('en')) {
62
- const enLoader = tool.entry.i18n['en' as keyof typeof tool.entry.i18n];
63
- const enContent = (await enLoader?.()) as ToolLocaleContent;
64
- enSlug = enContent.slug;
65
- }
66
-
67
- for (const locale of locales) {
68
- const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
69
- const content = (await loader?.()) as ToolLocaleContent;
70
- validateLocaleSlug({
71
- toolId: tool.entry.id,
72
- locale,
73
- content,
74
- enSlug,
75
- slugs,
76
- });
77
- }
78
- });
79
- });
80
- });
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
+
7
+ interface ValidateParams {
8
+ toolId: string;
9
+ locale: string;
10
+ content: ToolLocaleContent;
11
+ enSlug: string;
12
+ slugs: Map<string, string>;
13
+ }
14
+
15
+ const validateLocaleSlug = ({
16
+ toolId,
17
+ locale,
18
+ content,
19
+ enSlug,
20
+ slugs,
21
+ }: ValidateParams) => {
22
+ expect(
23
+ content.slug,
24
+ `Tool "${toolId}" locale "${locale}" has an invalid slug ("${content.slug}"). Slugs must be transliterated (only a-z, 0-9, and -).`,
25
+ ).toMatch(/^[a-z0-9-]+$/);
26
+
27
+ if (locale === 'en') {
28
+ return;
29
+ }
30
+
31
+ if (sharingLocales.includes(locale)) {
32
+ expect(
33
+ content.slug,
34
+ `Tool "${toolId}" locale "${locale}" must use the same slug as "en" ("${enSlug}").`,
35
+ ).toBe(enSlug);
36
+ } else {
37
+ expect(
38
+ content.slug,
39
+ `Tool "${toolId}" locale "${locale}" has the same slug as "en" ("${enSlug}"). Cada slug tiene que estar en su propia idioma`,
40
+ ).not.toBe(enSlug);
41
+
42
+ if (slugs.has(content.slug)) {
43
+ const previousLocale = slugs.get(content.slug);
44
+ expect(
45
+ false,
46
+ `Tool "${toolId}" locales "${locale}" and "${previousLocale}" share the same slug ("${content.slug}"). Cada slug tiene que estar en su propia idioma`,
47
+ ).toBe(true);
48
+ }
49
+ slugs.set(content.slug, locale);
50
+ }
51
+ };
52
+
53
+ describe('Slug Localization and Uniqueness Validation', () => {
54
+ ALL_TOOLS.forEach((tool) => {
55
+ describe(`Tool: ${tool.entry.id}`, () => {
56
+ it('every locale should have a unique, translated slug', async () => {
57
+ const slugs = new Map<string, string>();
58
+ const locales = Object.keys(tool.entry.i18n);
59
+
60
+ let enSlug = '';
61
+ if (locales.includes('en')) {
62
+ const enLoader = tool.entry.i18n['en' as keyof typeof tool.entry.i18n];
63
+ const enContent = (await enLoader?.()) as ToolLocaleContent;
64
+ enSlug = enContent.slug;
65
+ }
66
+
67
+ for (const locale of locales) {
68
+ const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
69
+ const content = (await loader?.()) as ToolLocaleContent;
70
+ validateLocaleSlug({
71
+ toolId: tool.entry.id,
72
+ locale,
73
+ content,
74
+ enSlug,
75
+ slugs,
76
+ });
77
+ }
78
+ });
79
+ });
80
+ });
81
81
  });
@@ -1,56 +1,56 @@
1
- import { describe, it } from 'vitest';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
-
5
- function getFiles(dir: string, ext: string[]): string[] {
6
- const results: string[] = [];
7
- if (!fs.existsSync(dir)) return results;
8
- const list = fs.readdirSync(dir);
9
- for (const file of list) {
10
- const fullPath = path.join(dir, file);
11
- const stat = fs.statSync(fullPath);
12
- if (stat && stat.isDirectory()) {
13
- results.push(...getFiles(fullPath, ext));
14
- } else if (ext.some((e) => file.endsWith(e))) {
15
- results.push(fullPath);
16
- }
17
- }
18
- return results;
19
- }
20
-
21
- const SRC_DIR = path.join(process.cwd(), 'src');
22
-
23
- describe('Project Titles - Separator Validation', () => {
24
- const files = [
25
- ...getFiles(path.join(SRC_DIR, 'tool'), ['.ts']),
26
- ...getFiles(path.join(SRC_DIR, 'category'), ['.ts']),
27
- ].filter(f => f.includes('i18n'));
28
-
29
- it.each(files)('Verify that titles in %s do not contain | or -', (filePath) => {
30
- const content = fs.readFileSync(filePath, 'utf-8');
31
- const relativePath = path.relative(process.cwd(), filePath);
32
-
33
- const titlePatterns = [
34
- /const\s+title\s*=\s*['"]([^'"]+)['"]/g,
35
- /title\s*:\s*['"]([^'"]+)['"]/g,
36
- ];
37
-
38
- const findings: string[] = [];
39
-
40
- for (const pattern of titlePatterns) {
41
- let match;
42
- while ((match = pattern.exec(content)) !== null) {
43
- const title = match[1];
44
- if (!title) continue;
45
- if (title.includes('|') || title.includes('-')) {
46
- findings.push(title);
47
- }
48
- }
49
- }
50
-
51
- if (findings.length > 0) {
52
- const list = findings.map((f) => ` - "${f}"`).join('\n');
53
- throw new Error(`Forbidden separators (| or -) found in titles in ${relativePath}:\n${list}`);
54
- }
55
- });
56
- });
1
+ import { describe, it } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ function getFiles(dir: string, ext: string[]): string[] {
6
+ const results: string[] = [];
7
+ if (!fs.existsSync(dir)) return results;
8
+ const list = fs.readdirSync(dir);
9
+ for (const file of list) {
10
+ const fullPath = path.join(dir, file);
11
+ const stat = fs.statSync(fullPath);
12
+ if (stat && stat.isDirectory()) {
13
+ results.push(...getFiles(fullPath, ext));
14
+ } else if (ext.some((e) => file.endsWith(e))) {
15
+ results.push(fullPath);
16
+ }
17
+ }
18
+ return results;
19
+ }
20
+
21
+ const SRC_DIR = path.join(process.cwd(), 'src');
22
+
23
+ describe('Project Titles - Separator Validation', () => {
24
+ const files = [
25
+ ...getFiles(path.join(SRC_DIR, 'tool'), ['.ts']),
26
+ ...getFiles(path.join(SRC_DIR, 'category'), ['.ts']),
27
+ ].filter(f => f.includes('i18n'));
28
+
29
+ it.each(files)('Verify that titles in %s do not contain | or -', (filePath) => {
30
+ const content = fs.readFileSync(filePath, 'utf-8');
31
+ const relativePath = path.relative(process.cwd(), filePath);
32
+
33
+ const titlePatterns = [
34
+ /const\s+title\s*=\s*['"]([^'"]+)['"]/g,
35
+ /title\s*:\s*['"]([^'"]+)['"]/g,
36
+ ];
37
+
38
+ const findings: string[] = [];
39
+
40
+ for (const pattern of titlePatterns) {
41
+ let match;
42
+ while ((match = pattern.exec(content)) !== null) {
43
+ const title = match[1];
44
+ if (!title) continue;
45
+ if (title.includes('|') || title.includes('-')) {
46
+ findings.push(title);
47
+ }
48
+ }
49
+ }
50
+
51
+ if (findings.length > 0) {
52
+ const list = findings.map((f) => ` - "${f}"`).join('\n');
53
+ throw new Error(`Forbidden separators (| or -) found in titles in ${relativePath}:\n${list}`);
54
+ }
55
+ });
56
+ });