@jjlmoya/utils-textiles 1.20.0 → 1.22.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.
- package/package.json +1 -1
- package/src/category/i18n/de.ts +76 -76
- package/src/category/i18n/en.ts +76 -76
- package/src/category/i18n/es.ts +76 -76
- package/src/category/i18n/fr.ts +76 -76
- package/src/category/i18n/id.ts +76 -76
- package/src/category/i18n/it.ts +76 -76
- package/src/category/i18n/ja.ts +76 -76
- package/src/category/i18n/ko.ts +76 -76
- package/src/category/i18n/nl.ts +76 -76
- package/src/category/i18n/pl.ts +76 -76
- package/src/category/i18n/pt.ts +76 -76
- package/src/category/i18n/ru.ts +76 -76
- package/src/category/i18n/sv.ts +76 -76
- package/src/category/i18n/zh.ts +76 -76
- package/src/category/index.ts +2 -0
- package/src/category/seo.astro +1 -1
- package/src/components/PreviewNavSidebar.astro +1 -1
- package/src/components/PreviewToolbar.astro +1 -1
- package/src/data.ts +1 -1
- package/src/entries.ts +4 -1
- package/src/env.d.ts +1 -1
- package/src/index.ts +1 -1
- package/src/layouts/PreviewLayout.astro +113 -113
- package/src/pages/index.astro +1 -1
- package/src/tests/faq_count.test.ts +19 -19
- package/src/tests/i18n_coverage.test.ts +4 -2
- package/src/tests/locale_spelling.test.ts +44 -0
- package/src/tests/mocks/astro_mock.js +1 -1
- package/src/tests/no_en_dash.test.ts +70 -70
- package/src/tests/no_h1_in_components.test.ts +1 -1
- package/src/tests/seo_length.test.ts +22 -22
- package/src/tests/seo_translation_completeness.test.ts +69 -0
- package/src/tests/spanish_leakage.test.ts +175 -153
- package/src/tests/tool_validation.test.ts +17 -17
- package/src/tool/burnTest/i18n/es.ts +211 -211
- package/src/tool/burnTest/i18n/fr.ts +211 -211
- package/src/tool/burnTest/i18n/ja.ts +211 -211
- package/src/tool/burnTest/i18n/ko.ts +211 -211
- package/src/tool/burnTest/i18n/pl.ts +211 -211
- package/src/tool/burnTest/i18n/ru.ts +211 -211
- package/src/tool/burnTest/i18n/sv.ts +211 -211
- package/src/tool/burnTest/i18n/zh.ts +211 -211
- package/src/tool/burnTest/seo.astro +15 -15
- package/src/tool/clothingSizeConverter/i18n/de.ts +348 -348
- package/src/tool/clothingSizeConverter/i18n/en.ts +348 -348
- package/src/tool/clothingSizeConverter/i18n/fr.ts +348 -348
- package/src/tool/clothingSizeConverter/i18n/ja.ts +348 -348
- package/src/tool/clothingSizeConverter/i18n/ko.ts +348 -348
- package/src/tool/clothingSizeConverter/i18n/nl.ts +348 -348
- package/src/tool/clothingSizeConverter/i18n/pl.ts +348 -348
- package/src/tool/clothingSizeConverter/i18n/ru.ts +348 -348
- package/src/tool/clothingSizeConverter/i18n/sv.ts +348 -348
- package/src/tool/clothingSizeConverter/i18n/tr.ts +348 -348
- package/src/tool/clothingSizeConverter/i18n/zh.ts +348 -348
- package/src/tool/clothingSizeConverter/seo.astro +15 -15
- package/src/tool/embroideryStitchPricingEstimator/bibliography.astro +6 -0
- package/src/tool/embroideryStitchPricingEstimator/bibliography.ts +12 -0
- package/src/tool/embroideryStitchPricingEstimator/component.astro +111 -0
- package/src/tool/embroideryStitchPricingEstimator/controller.ts +111 -0
- package/src/tool/embroideryStitchPricingEstimator/dom-views.ts +94 -0
- package/src/tool/embroideryStitchPricingEstimator/embroidery-stitch-count-pricing-estimator.css +438 -0
- package/src/tool/embroideryStitchPricingEstimator/entry.ts +29 -0
- package/src/tool/embroideryStitchPricingEstimator/evaluator.ts +22 -0
- package/src/tool/embroideryStitchPricingEstimator/i18n/de.ts +38 -0
- package/src/tool/embroideryStitchPricingEstimator/i18n/en.ts +194 -0
- package/src/tool/embroideryStitchPricingEstimator/i18n/es.ts +38 -0
- package/src/tool/embroideryStitchPricingEstimator/i18n/fr.ts +38 -0
- package/src/tool/embroideryStitchPricingEstimator/i18n/id.ts +38 -0
- package/src/tool/embroideryStitchPricingEstimator/i18n/it.ts +38 -0
- package/src/tool/embroideryStitchPricingEstimator/i18n/ja.ts +38 -0
- package/src/tool/embroideryStitchPricingEstimator/i18n/ko.ts +38 -0
- package/src/tool/embroideryStitchPricingEstimator/i18n/nl.ts +38 -0
- package/src/tool/embroideryStitchPricingEstimator/i18n/pl.ts +38 -0
- package/src/tool/embroideryStitchPricingEstimator/i18n/pt.ts +38 -0
- package/src/tool/embroideryStitchPricingEstimator/i18n/ru.ts +38 -0
- package/src/tool/embroideryStitchPricingEstimator/i18n/sv.ts +38 -0
- package/src/tool/embroideryStitchPricingEstimator/i18n/tr.ts +38 -0
- package/src/tool/embroideryStitchPricingEstimator/i18n/zh.ts +38 -0
- package/src/tool/embroideryStitchPricingEstimator/index.ts +11 -0
- package/src/tool/embroideryStitchPricingEstimator/logic.test.ts +40 -0
- package/src/tool/embroideryStitchPricingEstimator/logic.ts +86 -0
- package/src/tool/embroideryStitchPricingEstimator/seo.astro +15 -0
- package/src/tool/embroideryStitchPricingEstimator/storage.ts +20 -0
- package/src/tool/embroideryStitchPricingEstimator/ui.ts +53 -0
- package/src/tool/fabricProjectCalculator/i18n/de.ts +236 -236
- package/src/tool/fabricProjectCalculator/i18n/ja.ts +236 -236
- package/src/tool/fabricProjectCalculator/i18n/ko.ts +236 -236
- package/src/tool/fabricProjectCalculator/i18n/nl.ts +236 -236
- package/src/tool/fabricProjectCalculator/i18n/pl.ts +236 -236
- package/src/tool/fabricProjectCalculator/i18n/ru.ts +236 -236
- package/src/tool/fabricProjectCalculator/i18n/sv.ts +236 -236
- package/src/tool/fabricProjectCalculator/i18n/tr.ts +236 -236
- package/src/tool/fabricProjectCalculator/i18n/zh.ts +236 -236
- package/src/tool/fabricProjectCalculator/seo.astro +15 -15
- package/src/tool/fabricTruth/i18n/de.ts +268 -268
- package/src/tool/fabricTruth/i18n/en.ts +268 -268
- package/src/tool/fabricTruth/i18n/es.ts +302 -302
- package/src/tool/fabricTruth/i18n/fr.ts +268 -268
- package/src/tool/fabricTruth/i18n/id.ts +268 -268
- package/src/tool/fabricTruth/i18n/it.ts +268 -268
- package/src/tool/fabricTruth/i18n/ja.ts +268 -268
- package/src/tool/fabricTruth/i18n/ko.ts +268 -268
- package/src/tool/fabricTruth/i18n/nl.ts +268 -268
- package/src/tool/fabricTruth/i18n/pl.ts +268 -268
- package/src/tool/fabricTruth/i18n/pt.ts +268 -268
- package/src/tool/fabricTruth/i18n/ru.ts +268 -268
- package/src/tool/fabricTruth/i18n/sv.ts +268 -268
- package/src/tool/fabricTruth/i18n/tr.ts +268 -268
- package/src/tool/fabricTruth/i18n/zh.ts +272 -272
- package/src/tool/fabricTruth/seo.astro +15 -15
- package/src/tool/fiberPrep/i18n/de.ts +215 -215
- package/src/tool/fiberPrep/i18n/fr.ts +215 -215
- package/src/tool/fiberPrep/i18n/ja.ts +215 -215
- package/src/tool/fiberPrep/i18n/ko.ts +215 -215
- package/src/tool/fiberPrep/i18n/nl.ts +215 -215
- package/src/tool/fiberPrep/i18n/pl.ts +215 -215
- package/src/tool/fiberPrep/i18n/ru.ts +215 -215
- package/src/tool/fiberPrep/i18n/sv.ts +215 -215
- package/src/tool/fiberPrep/i18n/tr.ts +215 -215
- package/src/tool/fiberPrep/i18n/zh.ts +215 -215
- package/src/tool/fiberPrep/seo.astro +15 -15
- package/src/tool/knittingGauge/i18n/de.ts +224 -224
- package/src/tool/knittingGauge/i18n/ja.ts +224 -224
- package/src/tool/knittingGauge/i18n/ko.ts +224 -224
- package/src/tool/knittingGauge/i18n/pl.ts +224 -224
- package/src/tool/knittingGauge/i18n/ru.ts +224 -224
- package/src/tool/knittingGauge/i18n/tr.ts +224 -224
- package/src/tool/knittingGauge/i18n/zh.ts +224 -224
- package/src/tool/knittingGauge/seo.astro +15 -15
- package/src/tool/laundryGuide/i18n/de.ts +271 -271
- package/src/tool/laundryGuide/i18n/en.ts +271 -271
- package/src/tool/laundryGuide/i18n/es.ts +271 -271
- package/src/tool/laundryGuide/i18n/fr.ts +255 -255
- package/src/tool/laundryGuide/i18n/id.ts +271 -271
- package/src/tool/laundryGuide/i18n/it.ts +271 -271
- package/src/tool/laundryGuide/i18n/ja.ts +271 -271
- package/src/tool/laundryGuide/i18n/ko.ts +271 -271
- package/src/tool/laundryGuide/i18n/nl.ts +271 -271
- package/src/tool/laundryGuide/i18n/pl.ts +271 -271
- package/src/tool/laundryGuide/i18n/pt.ts +271 -271
- package/src/tool/laundryGuide/i18n/ru.ts +271 -271
- package/src/tool/laundryGuide/i18n/sv.ts +271 -271
- package/src/tool/laundryGuide/i18n/tr.ts +271 -271
- package/src/tool/laundryGuide/i18n/zh.ts +271 -271
- package/src/tool/laundryGuide/seo.astro +15 -15
- package/src/tool/needleConverter/i18n/de.ts +202 -202
- package/src/tool/needleConverter/i18n/es.ts +207 -207
- package/src/tool/needleConverter/i18n/fr.ts +207 -207
- package/src/tool/needleConverter/i18n/ja.ts +202 -202
- package/src/tool/needleConverter/i18n/ko.ts +202 -202
- package/src/tool/needleConverter/i18n/nl.ts +202 -202
- package/src/tool/needleConverter/i18n/pl.ts +202 -202
- package/src/tool/needleConverter/i18n/ru.ts +202 -202
- package/src/tool/needleConverter/i18n/sv.ts +202 -202
- package/src/tool/needleConverter/i18n/tr.ts +202 -202
- package/src/tool/needleConverter/i18n/zh.ts +202 -202
- package/src/tool/needleConverter/seo.astro +15 -15
- package/src/tool/sewingPatternScaler/component.astro +236 -236
- package/src/tool/sewingPatternScaler/i18n/fr.ts +170 -170
- package/src/tool/sewingPatternScaler/i18n/ja.ts +114 -114
- package/src/tool/sewingPatternScaler/i18n/ko.ts +114 -114
- package/src/tool/sewingPatternScaler/i18n/nl.ts +114 -114
- package/src/tool/sewingPatternScaler/i18n/pl.ts +114 -114
- package/src/tool/sewingPatternScaler/i18n/ru.ts +114 -114
- package/src/tool/sewingPatternScaler/i18n/sv.ts +114 -114
- package/src/tool/sewingPatternScaler/i18n/tr.ts +114 -114
- package/src/tool/sewingPatternScaler/i18n/zh.ts +114 -114
- package/src/tool/sewingPatternScaler/seo.astro +15 -15
- package/src/tool/shoeSizeConverter/i18n/fr.ts +183 -183
- package/src/tool/shoeSizeConverter/i18n/ja.ts +179 -179
- package/src/tool/shoeSizeConverter/i18n/ko.ts +179 -179
- package/src/tool/shoeSizeConverter/i18n/nl.ts +179 -179
- package/src/tool/shoeSizeConverter/i18n/pl.ts +179 -179
- package/src/tool/shoeSizeConverter/i18n/ru.ts +179 -179
- package/src/tool/shoeSizeConverter/i18n/sv.ts +179 -179
- package/src/tool/shoeSizeConverter/i18n/tr.ts +179 -179
- package/src/tool/shoeSizeConverter/i18n/zh.ts +179 -179
- package/src/tool/shoeSizeConverter/seo.astro +15 -15
- package/src/tool/stainChemistry/bibliography.ts +16 -16
- package/src/tool/stainChemistry/i18n/de.ts +286 -286
- package/src/tool/stainChemistry/i18n/en.ts +289 -289
- package/src/tool/stainChemistry/i18n/es.ts +286 -286
- package/src/tool/stainChemistry/i18n/fr.ts +296 -296
- package/src/tool/stainChemistry/i18n/id.ts +286 -286
- package/src/tool/stainChemistry/i18n/it.ts +286 -286
- package/src/tool/stainChemistry/i18n/ja.ts +286 -286
- package/src/tool/stainChemistry/i18n/ko.ts +283 -283
- package/src/tool/stainChemistry/i18n/nl.ts +286 -286
- package/src/tool/stainChemistry/i18n/pl.ts +286 -286
- package/src/tool/stainChemistry/i18n/pt.ts +286 -286
- package/src/tool/stainChemistry/i18n/ru.ts +286 -286
- package/src/tool/stainChemistry/i18n/sv.ts +286 -286
- package/src/tool/stainChemistry/i18n/tr.ts +286 -286
- package/src/tool/stainChemistry/i18n/zh.ts +281 -281
- package/src/tool/stainChemistry/seo.astro +15 -15
- package/src/tool/yarnCalculator/i18n/de.ts +296 -296
- package/src/tool/yarnCalculator/i18n/es.ts +296 -296
- package/src/tool/yarnCalculator/i18n/fr.ts +296 -296
- package/src/tool/yarnCalculator/i18n/ja.ts +296 -296
- package/src/tool/yarnCalculator/i18n/ko.ts +296 -296
- package/src/tool/yarnCalculator/i18n/nl.ts +296 -296
- package/src/tool/yarnCalculator/i18n/pl.ts +296 -296
- package/src/tool/yarnCalculator/i18n/ru.ts +296 -296
- package/src/tool/yarnCalculator/i18n/sv.ts +296 -296
- package/src/tool/yarnCalculator/i18n/tr.ts +296 -296
- package/src/tool/yarnCalculator/i18n/zh.ts +295 -295
- package/src/tool/yarnCalculator/seo.astro +15 -15
- package/src/tools.ts +2 -2
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import type * as DATA from '../data';
|
|
3
|
-
|
|
4
|
-
const TOOLS: typeof DATA.textilesCategory[] = [];
|
|
5
|
-
|
|
6
|
-
describe('FAQ Content Validation', () => {
|
|
7
|
-
TOOLS.forEach((entry) => {
|
|
8
|
-
describe(`Tool: ${entry.icon}`, () => {
|
|
9
|
-
it('placeholder', () => {
|
|
10
|
-
expect(true).toBe(true);
|
|
11
|
-
});
|
|
12
|
-
});
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('no tools registered yet', () => {
|
|
16
|
-
expect(TOOLS.length).toBe(0);
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type * as DATA from '../data';
|
|
3
|
+
|
|
4
|
+
const TOOLS: typeof DATA.textilesCategory[] = [];
|
|
5
|
+
|
|
6
|
+
describe('FAQ Content Validation', () => {
|
|
7
|
+
TOOLS.forEach((entry) => {
|
|
8
|
+
describe(`Tool: ${entry.icon}`, () => {
|
|
9
|
+
it('placeholder', () => {
|
|
10
|
+
expect(true).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('no tools registered yet', () => {
|
|
16
|
+
expect(TOOLS.length).toBe(0);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
@@ -12,9 +12,11 @@ describe('I18n Coverage Validation', () => {
|
|
|
12
12
|
|
|
13
13
|
ALL_TOOLS.forEach(({ entry }: { entry: any }) => {
|
|
14
14
|
describe(`Tool: ${entry.id}`, () => {
|
|
15
|
+
const localesToCheck = EXPECTED_LOCALES;
|
|
16
|
+
|
|
15
17
|
it('should have all 15 required locales', () => {
|
|
16
18
|
const registeredLocales = Object.keys(entry.i18n);
|
|
17
|
-
|
|
19
|
+
localesToCheck.forEach((locale) => {
|
|
18
20
|
expect(
|
|
19
21
|
registeredLocales,
|
|
20
22
|
`Tool "${entry.id}" is missing locale "${locale}"`,
|
|
@@ -23,7 +25,7 @@ describe('I18n Coverage Validation', () => {
|
|
|
23
25
|
});
|
|
24
26
|
|
|
25
27
|
it('all locale loaders should be functions', () => {
|
|
26
|
-
|
|
28
|
+
localesToCheck.forEach((locale) => {
|
|
27
29
|
const loader = entry.i18n[locale as keyof typeof entry.i18n];
|
|
28
30
|
expect(
|
|
29
31
|
typeof loader,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
|
|
4
|
+
const TOOL_ID = 'embroidery-stitch-pricing-estimator';
|
|
5
|
+
const LOCALES = ['de', 'en', 'es', 'fr', 'id', 'it', 'ja', 'ko', 'nl', 'pl', 'pt', 'ru', 'sv', 'tr', 'zh'] as const;
|
|
6
|
+
const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
|
|
7
|
+
const FORBIDDEN_TERMS: Partial<Record<(typeof LOCALES)[number], RegExp>> = {
|
|
8
|
+
de: /\b(?:fuer|schaetze|schaetzung|flaeche|hoehe|groesse|waehle|zaehle|waehrung|fuell|saeulen|raender|berueck|probestueck|pruef|ergaenz|veraend|umruest)\w*\b/i,
|
|
9
|
+
es: /\b(?:produccion|estimacion|tamano|diseno|maquina|preparacion|digitalizacion|numero|despues|planificacion|segun|anade|area)\b/i,
|
|
10
|
+
fr: /\b(?:l estimation|d oeuvre|t il|s agit|l effort|l echelle|l application|l exemple|contient il|seront ils)\b/i,
|
|
11
|
+
id: /\b(?:benar benar|menyetik)\b/i,
|
|
12
|
+
it: /\b(?:l impegno|all ora|un impostazione|l applicazione|l operatore|d una|d oeuvre|s agit|l esempio)\b/i,
|
|
13
|
+
nl: /\bvaluta eenheden\b/i,
|
|
14
|
+
pl: /\b(?:sciegow|scieg|gestosci|wspolczynnik\w*|wypeln\w*|oszacowac|porown\w*|dlugo|sprawdz|zaloz\w*|predkosc\w*|kolorow)\b/i,
|
|
15
|
+
sv: /\b(?:Tatafyllnad|Brodderiyta)\b/i,
|
|
16
|
+
};
|
|
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 localeText(content: Record<string, unknown>): string {
|
|
26
|
+
return TRANSLATABLE_KEYS.flatMap((key) => collectStrings(content[key])).join(' ');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('Locale spelling regression', () => {
|
|
30
|
+
it('keeps the reviewed embroidery translations free of known transcription errors', async () => {
|
|
31
|
+
const tool = ALL_TOOLS.find((item) => item.entry.id === TOOL_ID);
|
|
32
|
+
expect(tool).toBeDefined();
|
|
33
|
+
if (!tool) return;
|
|
34
|
+
|
|
35
|
+
for (const locale of LOCALES) {
|
|
36
|
+
const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
|
|
37
|
+
expect(loader, `Missing loader for ${locale}`).toBeDefined();
|
|
38
|
+
if (!loader) continue;
|
|
39
|
+
const content = await loader();
|
|
40
|
+
const pattern = FORBIDDEN_TERMS[locale];
|
|
41
|
+
expect(pattern?.test(localeText(content as unknown as Record<string, unknown>)) ?? false, `Known spelling regression in ${locale}`).toBe(false);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export default function () { return null; }
|
|
2
|
-
|
|
2
|
+
|
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
function getFiles(dir: string): string[] {
|
|
6
|
-
const results: string[] = [];
|
|
7
|
-
if (!fs.existsSync(dir)) {
|
|
8
|
-
return results;
|
|
9
|
-
}
|
|
10
|
-
const list = fs.readdirSync(dir);
|
|
11
|
-
for (const file of list) {
|
|
12
|
-
const fullPath = path.join(dir, file);
|
|
13
|
-
const stat = fs.statSync(fullPath);
|
|
14
|
-
if (stat && stat.isDirectory()) {
|
|
15
|
-
if (file !== 'tests' && file !== 'node_modules' && file !== '.astro') {
|
|
16
|
-
results.push(...getFiles(fullPath));
|
|
17
|
-
}
|
|
18
|
-
} else {
|
|
19
|
-
results.push(fullPath);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return results;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function isContentFile(filePath: string): boolean {
|
|
26
|
-
return /\\i18n\\/.test(filePath) || filePath.endsWith('bibliography.ts');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const srcDir = path.join(process.cwd(), 'src');
|
|
30
|
-
const scriptsDir = path.join(process.cwd(), 'scripts');
|
|
31
|
-
const filesToTest = [
|
|
32
|
-
...getFiles(srcDir).filter(isContentFile),
|
|
33
|
-
...getFiles(scriptsDir).filter(isContentFile),
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
const aiTypographyGarbage = [
|
|
37
|
-
'\u2013',
|
|
38
|
-
'\u2014',
|
|
39
|
-
'\u2026',
|
|
40
|
-
'\u201C',
|
|
41
|
-
'\u201D',
|
|
42
|
-
'\u2018',
|
|
43
|
-
'\u2019',
|
|
44
|
-
'\u00AB',
|
|
45
|
-
'\u00BB',
|
|
46
|
-
'\u200B',
|
|
47
|
-
'\u201E',
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
describe('Typography Garbage Character Validation', () => {
|
|
51
|
-
filesToTest.forEach((filePath) => {
|
|
52
|
-
const relativePath = path.relative(process.cwd(), filePath);
|
|
53
|
-
it(`should not contain typography garbage characters in ${relativePath}`, () => {
|
|
54
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
55
|
-
const hasAiPatterns = aiTypographyGarbage.some(char => content.includes(char));
|
|
56
|
-
expect(hasAiPatterns).toBe(false);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it(`should not contain space before colon in ${relativePath}`, () => {
|
|
60
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
61
|
-
const spaceBeforeColon = / : /.test(content);
|
|
62
|
-
expect(spaceBeforeColon).toBe(false);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it(`should not contain double hyphen in ${relativePath}`, () => {
|
|
66
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
67
|
-
expect(content).not.toContain('--');
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
});
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
function getFiles(dir: string): string[] {
|
|
6
|
+
const results: string[] = [];
|
|
7
|
+
if (!fs.existsSync(dir)) {
|
|
8
|
+
return results;
|
|
9
|
+
}
|
|
10
|
+
const list = fs.readdirSync(dir);
|
|
11
|
+
for (const file of list) {
|
|
12
|
+
const fullPath = path.join(dir, file);
|
|
13
|
+
const stat = fs.statSync(fullPath);
|
|
14
|
+
if (stat && stat.isDirectory()) {
|
|
15
|
+
if (file !== 'tests' && file !== 'node_modules' && file !== '.astro') {
|
|
16
|
+
results.push(...getFiles(fullPath));
|
|
17
|
+
}
|
|
18
|
+
} else {
|
|
19
|
+
results.push(fullPath);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return results;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isContentFile(filePath: string): boolean {
|
|
26
|
+
return /\\i18n\\/.test(filePath) || filePath.endsWith('bibliography.ts');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const srcDir = path.join(process.cwd(), 'src');
|
|
30
|
+
const scriptsDir = path.join(process.cwd(), 'scripts');
|
|
31
|
+
const filesToTest = [
|
|
32
|
+
...getFiles(srcDir).filter(isContentFile),
|
|
33
|
+
...getFiles(scriptsDir).filter(isContentFile),
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const aiTypographyGarbage = [
|
|
37
|
+
'\u2013',
|
|
38
|
+
'\u2014',
|
|
39
|
+
'\u2026',
|
|
40
|
+
'\u201C',
|
|
41
|
+
'\u201D',
|
|
42
|
+
'\u2018',
|
|
43
|
+
'\u2019',
|
|
44
|
+
'\u00AB',
|
|
45
|
+
'\u00BB',
|
|
46
|
+
'\u200B',
|
|
47
|
+
'\u201E',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
describe('Typography Garbage Character Validation', () => {
|
|
51
|
+
filesToTest.forEach((filePath) => {
|
|
52
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
53
|
+
it(`should not contain typography garbage characters in ${relativePath}`, () => {
|
|
54
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
55
|
+
const hasAiPatterns = aiTypographyGarbage.some(char => content.includes(char));
|
|
56
|
+
expect(hasAiPatterns).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it(`should not contain space before colon in ${relativePath}`, () => {
|
|
60
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
61
|
+
const spaceBeforeColon = / : /.test(content);
|
|
62
|
+
expect(spaceBeforeColon).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it(`should not contain double hyphen in ${relativePath}`, () => {
|
|
66
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
67
|
+
expect(content).not.toContain('--');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import * as DATA from '../data';
|
|
3
|
-
|
|
4
|
-
const ENTRIES = [
|
|
5
|
-
{ id: 'textilesCategory', i18n: DATA.textilesCategory.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
|
-
const content = await loader();
|
|
15
|
-
if (!content.seo) return;
|
|
16
|
-
expect(Array.isArray(content.seo)).toBe(true);
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as DATA from '../data';
|
|
3
|
+
|
|
4
|
+
const ENTRIES = [
|
|
5
|
+
{ id: 'textilesCategory', i18n: DATA.textilesCategory.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
|
+
const content = await loader();
|
|
15
|
+
if (!content.seo) return;
|
|
16
|
+
expect(Array.isArray(content.seo)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
|
|
4
|
+
const ASIAN_LOCALES = ['ja', 'ko', 'zh'];
|
|
5
|
+
|
|
6
|
+
function getSimpleSectionLength(section: any): number {
|
|
7
|
+
if (section.type === 'title') return section.text ? section.text.length : 0;
|
|
8
|
+
if (section.type === 'paragraph') return section.html ? section.html.replace(/<[^>]*>/g, '').length : 0;
|
|
9
|
+
if (section.type === 'list') return section.items ? section.items.join('').length : 0;
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getComplexSectionLength(section: any): number {
|
|
14
|
+
if (section.type === 'diagnostic' || section.type === 'tip') {
|
|
15
|
+
return (section.title ? section.title.length : 0) + (section.html ? section.html.replace(/<[^>]*>/g, '').length : 0);
|
|
16
|
+
}
|
|
17
|
+
if (section.type === 'table') {
|
|
18
|
+
return ((section.headers || []).join('').length) + ((section.rows || []).flat().join('').length);
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getSectionLength(section: any): number {
|
|
24
|
+
return getSimpleSectionLength(section) || getComplexSectionLength(section);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function calculateSeoTextLength(seoSections: any[]): number {
|
|
28
|
+
return seoSections.reduce((acc, section) => acc + getSectionLength(section), 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function checkLocaleSeoLength(toolId: string, locale: string, localeLen: number, enLen: number): void {
|
|
32
|
+
const isAsian = ASIAN_LOCALES.includes(locale);
|
|
33
|
+
const minLength = Math.floor(enLen * (isAsian ? 0.25 : 0.70));
|
|
34
|
+
const msgType = isAsian ? 'suspiciously short' : 'truncated/lazy';
|
|
35
|
+
|
|
36
|
+
expect(
|
|
37
|
+
localeLen,
|
|
38
|
+
`[LAZY SEO TRANSLATION] Tool "${toolId}" locale "${locale}" SEO text is ${msgType} (${localeLen} chars vs EN ${enLen} chars, expected min ${minLength})`,
|
|
39
|
+
).toBeGreaterThanOrEqual(minLength);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function auditSingleLocale(toolId: string, locale: string, loader: any, enSeoLength: number): Promise<void> {
|
|
43
|
+
if (locale === 'en' || !loader) return;
|
|
44
|
+
const content = await loader();
|
|
45
|
+
if (!content.seo || !Array.isArray(content.seo)) return;
|
|
46
|
+
|
|
47
|
+
checkLocaleSeoLength(toolId, locale, calculateSeoTextLength(content.seo), enSeoLength);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('SEO Translation Completeness & Laziness Audit', () => {
|
|
51
|
+
ALL_TOOLS.forEach(({ entry }) => {
|
|
52
|
+
describe(`Tool: ${entry.id}`, () => {
|
|
53
|
+
it('should not have lazy or truncated SEO content compared to English reference', async () => {
|
|
54
|
+
const enLoader = entry.i18n['en' as keyof typeof entry.i18n];
|
|
55
|
+
if (!enLoader) return;
|
|
56
|
+
|
|
57
|
+
const enContent = await enLoader();
|
|
58
|
+
if (!enContent.seo || !Array.isArray(enContent.seo)) return;
|
|
59
|
+
|
|
60
|
+
const enSeoLength = calculateSeoTextLength(enContent.seo);
|
|
61
|
+
|
|
62
|
+
for (const [locale, loader] of Object.entries(entry.i18n)) {
|
|
63
|
+
await auditSingleLocale(entry.id, locale, loader, enSeoLength);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|