@jjlmoya/utils-nautical 1.15.0 → 1.17.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 +1 -1
- package/src/category/i18n/en.ts +1 -1
- package/src/category/i18n/es.ts +16 -16
- package/src/category/i18n/fr.ts +16 -16
- package/src/category/i18n/id.ts +1 -1
- package/src/category/i18n/it.ts +1 -1
- package/src/category/i18n/nl.ts +1 -1
- package/src/category/i18n/pl.ts +1 -1
- package/src/category/i18n/pt.ts +1 -1
- package/src/category/i18n/ru.ts +3 -3
- package/src/category/i18n/sv.ts +1 -1
- package/src/category/i18n/tr.ts +1 -1
- package/src/layouts/PreviewLayout.astro +1 -0
- package/src/pages/[locale]/[slug].astro +28 -12
- package/src/tests/diacritics_density.test.ts +118 -0
- package/src/tests/inverted_punctuation.test.ts +84 -0
- package/src/tests/locale_completeness.test.ts +1 -31
- package/src/tests/no_en_dash.test.ts +70 -0
- package/src/tests/script_density.test.ts +94 -0
- package/src/tests/shared-test-helpers.ts +56 -0
- package/src/tests/tool_exports.test.ts +34 -0
- package/src/tool/endurance/bibliography.astro +1 -1
- package/src/tool/endurance/bibliography.ts +12 -0
- package/src/tool/endurance/entry.ts +2 -2
- package/src/tool/endurance/i18n/de.ts +1 -13
- package/src/tool/endurance/i18n/en.ts +1 -12
- package/src/tool/endurance/i18n/es.ts +55 -67
- package/src/tool/endurance/i18n/fr.ts +71 -83
- package/src/tool/endurance/i18n/id.ts +1 -13
- package/src/tool/endurance/i18n/it.ts +1 -13
- package/src/tool/endurance/i18n/ja.ts +1 -13
- package/src/tool/endurance/i18n/ko.ts +1 -13
- package/src/tool/endurance/i18n/nl.ts +1 -13
- package/src/tool/endurance/i18n/pl.ts +1 -13
- package/src/tool/endurance/i18n/pt.ts +1 -13
- package/src/tool/endurance/i18n/ru.ts +10 -22
- package/src/tool/endurance/i18n/sv.ts +1 -13
- package/src/tool/endurance/i18n/tr.ts +1 -13
- package/src/tool/endurance/i18n/zh.ts +1 -13
- package/src/tool/endurance/seo.astro +2 -42
- package/src/tool/nauticalConverter/bibliography.astro +1 -1
- package/src/tool/nauticalConverter/bibliography.ts +12 -0
- package/src/tool/nauticalConverter/entry.ts +2 -2
- package/src/tool/nauticalConverter/i18n/de.ts +1 -13
- package/src/tool/nauticalConverter/i18n/en.ts +1 -13
- package/src/tool/nauticalConverter/i18n/es.ts +47 -59
- package/src/tool/nauticalConverter/i18n/fr.ts +54 -66
- package/src/tool/nauticalConverter/i18n/id.ts +1 -13
- package/src/tool/nauticalConverter/i18n/it.ts +1 -13
- package/src/tool/nauticalConverter/i18n/ja.ts +1 -13
- package/src/tool/nauticalConverter/i18n/ko.ts +1 -13
- package/src/tool/nauticalConverter/i18n/nl.ts +1 -13
- package/src/tool/nauticalConverter/i18n/pl.ts +4 -16
- package/src/tool/nauticalConverter/i18n/pt.ts +1 -13
- package/src/tool/nauticalConverter/i18n/ru.ts +9 -21
- package/src/tool/nauticalConverter/i18n/sv.ts +1 -13
- package/src/tool/nauticalConverter/i18n/tr.ts +1 -13
- package/src/tool/nauticalConverter/i18n/zh.ts +4 -16
- package/src/tool/nauticalConverter/seo.astro +2 -39
- package/src/tool/sailArea/bibliography.astro +1 -1
- package/src/tool/sailArea/bibliography.ts +16 -0
- package/src/tool/sailArea/entry.ts +2 -2
- package/src/tool/sailArea/i18n/de.ts +1 -17
- package/src/tool/sailArea/i18n/en.ts +1 -17
- package/src/tool/sailArea/i18n/es.ts +54 -70
- package/src/tool/sailArea/i18n/fr.ts +71 -87
- package/src/tool/sailArea/i18n/id.ts +1 -17
- package/src/tool/sailArea/i18n/it.ts +1 -17
- package/src/tool/sailArea/i18n/ja.ts +1 -17
- package/src/tool/sailArea/i18n/ko.ts +1 -17
- package/src/tool/sailArea/i18n/nl.ts +1 -17
- package/src/tool/sailArea/i18n/pl.ts +1 -17
- package/src/tool/sailArea/i18n/pt.ts +1 -17
- package/src/tool/sailArea/i18n/ru.ts +9 -25
- package/src/tool/sailArea/i18n/sv.ts +1 -17
- package/src/tool/sailArea/i18n/tr.ts +1 -17
- package/src/tool/sailArea/i18n/zh.ts +6 -22
- package/src/tool/sailArea/seo.astro +2 -39
- package/src/tool/speedConverter/bibliography.astro +1 -1
- package/src/tool/speedConverter/bibliography.ts +12 -0
- package/src/tool/speedConverter/entry.ts +2 -2
- package/src/tool/speedConverter/i18n/de.ts +1 -13
- package/src/tool/speedConverter/i18n/en.ts +1 -13
- package/src/tool/speedConverter/i18n/es.ts +59 -71
- package/src/tool/speedConverter/i18n/fr.ts +76 -88
- package/src/tool/speedConverter/i18n/id.ts +1 -13
- package/src/tool/speedConverter/i18n/it.ts +1 -13
- package/src/tool/speedConverter/i18n/ja.ts +1 -13
- package/src/tool/speedConverter/i18n/ko.ts +1 -13
- package/src/tool/speedConverter/i18n/nl.ts +1 -13
- package/src/tool/speedConverter/i18n/pl.ts +1 -13
- package/src/tool/speedConverter/i18n/pt.ts +1 -13
- package/src/tool/speedConverter/i18n/ru.ts +7 -19
- package/src/tool/speedConverter/i18n/sv.ts +1 -13
- package/src/tool/speedConverter/i18n/tr.ts +1 -13
- package/src/tool/speedConverter/i18n/zh.ts +6 -18
- package/src/tool/speedConverter/seo.astro +2 -30
- package/src/tool/tideCalculator/bibliography.astro +1 -1
- package/src/tool/tideCalculator/bibliography.ts +12 -0
- package/src/tool/tideCalculator/entry.ts +2 -2
- package/src/tool/tideCalculator/i18n/de.ts +1 -13
- package/src/tool/tideCalculator/i18n/en.ts +1 -13
- package/src/tool/tideCalculator/i18n/es.ts +40 -52
- package/src/tool/tideCalculator/i18n/fr.ts +44 -56
- package/src/tool/tideCalculator/i18n/id.ts +1 -13
- package/src/tool/tideCalculator/i18n/it.ts +1 -13
- package/src/tool/tideCalculator/i18n/ja.ts +1 -13
- package/src/tool/tideCalculator/i18n/ko.ts +1 -13
- package/src/tool/tideCalculator/i18n/nl.ts +1 -13
- package/src/tool/tideCalculator/i18n/pl.ts +1 -13
- package/src/tool/tideCalculator/i18n/pt.ts +1 -13
- package/src/tool/tideCalculator/i18n/ru.ts +7 -19
- package/src/tool/tideCalculator/i18n/sv.ts +1 -13
- package/src/tool/tideCalculator/i18n/tr.ts +1 -13
- package/src/tool/tideCalculator/i18n/zh.ts +2 -14
- package/src/tool/tideCalculator/seo.astro +2 -48
- package/src/tool/tideCalculator/tide-height-calculator.css +136 -5
- package/src/tool/underKeel/bibliography.astro +1 -1
- package/src/tool/underKeel/bibliography.ts +12 -0
- package/src/tool/underKeel/entry.ts +2 -2
- package/src/tool/underKeel/i18n/de.ts +1 -13
- package/src/tool/underKeel/i18n/en.ts +1 -13
- package/src/tool/underKeel/i18n/es.ts +40 -52
- package/src/tool/underKeel/i18n/fr.ts +46 -58
- package/src/tool/underKeel/i18n/id.ts +2 -14
- package/src/tool/underKeel/i18n/it.ts +2 -14
- package/src/tool/underKeel/i18n/ja.ts +2 -14
- package/src/tool/underKeel/i18n/ko.ts +2 -14
- package/src/tool/underKeel/i18n/nl.ts +2 -14
- package/src/tool/underKeel/i18n/pl.ts +2 -14
- package/src/tool/underKeel/i18n/pt.ts +2 -14
- package/src/tool/underKeel/i18n/ru.ts +7 -19
- package/src/tool/underKeel/i18n/sv.ts +2 -14
- package/src/tool/underKeel/i18n/tr.ts +2 -14
- package/src/tool/underKeel/i18n/zh.ts +2 -14
- package/src/tool/underKeel/seo.astro +2 -33
|
@@ -1,39 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { ALL_TOOLS } from '../tools';
|
|
3
|
-
import type { ToolLocaleContent } from '../types';
|
|
4
3
|
|
|
5
|
-
describe('Locale Completeness Validation', () => {
|
|
6
|
-
ALL_TOOLS.forEach((tool) => {
|
|
7
|
-
describe(`Tool: ${tool.entry.id}`, () => {
|
|
8
|
-
Object.keys(tool.entry.i18n).forEach((locale) => {
|
|
9
|
-
describe(`Locale: ${locale}`, () => {
|
|
10
|
-
it('faqTitle should be defined when faq items exist', async () => {
|
|
11
|
-
const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
|
|
12
|
-
const content = (await loader?.()) as ToolLocaleContent;
|
|
13
|
-
|
|
14
|
-
if (content.faq.length > 0) {
|
|
15
|
-
expect(
|
|
16
|
-
content.ui.faqTitle,
|
|
17
|
-
`Tool "${tool.entry.id}" locale "${locale}" has ${content.faq.length} FAQ items but is missing faqTitle in ui`,
|
|
18
|
-
).toBeTruthy();
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
4
|
|
|
22
|
-
|
|
23
|
-
const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
|
|
24
|
-
const content = (await loader?.()) as ToolLocaleContent;
|
|
5
|
+
describe('Locale Completeness Validation', () => {
|
|
25
6
|
|
|
26
|
-
if (content.bibliography.length > 0) {
|
|
27
|
-
expect(
|
|
28
|
-
content.ui.bibliographyTitle,
|
|
29
|
-
`Tool "${tool.entry.id}" locale "${locale}" has ${content.bibliography.length} bibliography items but is missing bibliographyTitle in ui`,
|
|
30
|
-
).toBeTruthy();
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
7
|
|
|
38
8
|
it('all 10 tools registered', () => {
|
|
39
9
|
expect(ALL_TOOLS.length).toBe(6);
|
|
@@ -0,0 +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
|
+
});
|
|
@@ -0,0 +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 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
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface ToolExportValidationResult {
|
|
4
|
+
passed: boolean;
|
|
5
|
+
failures: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function validateComponentType(
|
|
9
|
+
toolId: string,
|
|
10
|
+
componentName: string,
|
|
11
|
+
component: unknown,
|
|
12
|
+
failures: string[],
|
|
13
|
+
): void {
|
|
14
|
+
if (typeof component !== 'function') {
|
|
15
|
+
failures.push(`${toolId}: ${componentName} is not a function (${typeof component})`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function validateComponentExecution(
|
|
20
|
+
toolId: string,
|
|
21
|
+
componentName: string,
|
|
22
|
+
fn: () => Promise<unknown>,
|
|
23
|
+
failures: string[],
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
try {
|
|
26
|
+
const result = await fn();
|
|
27
|
+
if (!result || typeof result !== 'object') {
|
|
28
|
+
failures.push(`${toolId}: ${componentName} import returned invalid result`);
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
failures.push(`${toolId}: ${componentName} execution error - ${error instanceof Error ? error.message : 'unknown'}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function validateToolExports(tools: ToolDefinition[]): Promise<ToolExportValidationResult> {
|
|
36
|
+
const failures: string[] = [];
|
|
37
|
+
|
|
38
|
+
for (const tool of tools) {
|
|
39
|
+
validateComponentType(tool.entry.id, 'Component', tool.Component, failures);
|
|
40
|
+
validateComponentType(tool.entry.id, 'SEOComponent', tool.SEOComponent, failures);
|
|
41
|
+
validateComponentType(tool.entry.id, 'BibliographyComponent', tool.BibliographyComponent, failures);
|
|
42
|
+
|
|
43
|
+
const componentFn = tool.Component as () => Promise<unknown>;
|
|
44
|
+
const seoFn = tool.SEOComponent as () => Promise<unknown>;
|
|
45
|
+
const bibFn = tool.BibliographyComponent as () => Promise<unknown>;
|
|
46
|
+
|
|
47
|
+
await validateComponentExecution(tool.entry.id, 'Component', componentFn, failures);
|
|
48
|
+
await validateComponentExecution(tool.entry.id, 'SEOComponent', seoFn, failures);
|
|
49
|
+
await validateComponentExecution(tool.entry.id, 'BibliographyComponent', bibFn, failures);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
passed: failures.length === 0,
|
|
54
|
+
failures,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
import { validateToolExports } from './shared-test-helpers';
|
|
4
|
+
|
|
5
|
+
describe('Tool Exports Pattern Validation', () => {
|
|
6
|
+
describe('Component Exports Format', () => {
|
|
7
|
+
ALL_TOOLS.forEach((tool) => {
|
|
8
|
+
it(`${tool.entry.id}: Component should be a lazy-loaded function`, () => {
|
|
9
|
+
expect(typeof tool.Component).toBe('function');
|
|
10
|
+
expect(tool.Component).toBeInstanceOf(Function);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it(`${tool.entry.id}: SEOComponent should be a lazy-loaded function`, () => {
|
|
14
|
+
expect(typeof tool.SEOComponent).toBe('function');
|
|
15
|
+
expect(tool.SEOComponent).toBeInstanceOf(Function);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it(`${tool.entry.id}: BibliographyComponent should be a lazy-loaded function`, () => {
|
|
19
|
+
expect(typeof tool.BibliographyComponent).toBe('function');
|
|
20
|
+
expect(tool.BibliographyComponent).toBeInstanceOf(Function);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('Dynamic Import Validation', () => {
|
|
26
|
+
it('all tools must have functional dynamic imports', async () => {
|
|
27
|
+
const result = await validateToolExports(ALL_TOOLS);
|
|
28
|
+
if (!result.passed) {
|
|
29
|
+
throw new Error(`Tool export validation failed:\n${result.failures.join('\n')}`);
|
|
30
|
+
}
|
|
31
|
+
expect(result.passed).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -11,4 +11,4 @@ const { locale = 'es' } = Astro.props;
|
|
|
11
11
|
const content = await endurance.i18n[locale]?.();
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
{content && <SharedBibliography links={content.bibliography}
|
|
14
|
+
{content && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'UK Maritime and Coastguard Agency',
|
|
6
|
+
url: 'https://www.gov.uk/government/organisations/maritime-and-coastguard-agency',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'RYA - Royal Yachting Association',
|
|
10
|
+
url: 'https://www.rya.org.uk/',
|
|
11
|
+
},
|
|
12
|
+
];
|
|
@@ -24,8 +24,6 @@ export interface EnduranceUI {
|
|
|
24
24
|
seaLight: string;
|
|
25
25
|
seaModerate: string;
|
|
26
26
|
seaStorm: string;
|
|
27
|
-
faqTitle: string;
|
|
28
|
-
bibliographyTitle: string;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
export type EnduranceLocaleContent = ToolLocaleContent<EnduranceUI>;
|
|
@@ -54,3 +52,5 @@ export const endurance: NauticalToolEntry<EnduranceUI> = {
|
|
|
54
52
|
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
55
53
|
},
|
|
56
54
|
};
|
|
55
|
+
|
|
56
|
+
export { bibliography } from './bibliography';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
1
2
|
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
2
3
|
import type { EnduranceUI, EnduranceLocaleContent } from '../index';
|
|
3
4
|
|
|
@@ -29,8 +30,6 @@ const ui: EnduranceUI = {
|
|
|
29
30
|
seaLight: 'Leichter Wellengang (+15%)',
|
|
30
31
|
seaModerate: 'Mäßiger Wellengang (+30%)',
|
|
31
32
|
seaStorm: 'Sturm (+60%)',
|
|
32
|
-
faqTitle: 'Häufig gestellte Fragen',
|
|
33
|
-
bibliographyTitle: 'Bibliografie',
|
|
34
33
|
};
|
|
35
34
|
|
|
36
35
|
const faq: EnduranceLocaleContent['faq'] = [
|
|
@@ -75,17 +74,6 @@ const howTo: EnduranceLocaleContent['howTo'] = [
|
|
|
75
74
|
},
|
|
76
75
|
];
|
|
77
76
|
|
|
78
|
-
const bibliography: EnduranceLocaleContent['bibliography'] = [
|
|
79
|
-
{
|
|
80
|
-
name: 'UK Maritime and Coastguard Agency',
|
|
81
|
-
url: 'https://www.gov.uk/government/organisations/maritime-and-coastguard-agency',
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
name: 'RYA - Royal Yachting Association',
|
|
85
|
-
url: 'https://www.rya.org.uk/',
|
|
86
|
-
},
|
|
87
|
-
];
|
|
88
|
-
|
|
89
77
|
const seo: EnduranceLocaleContent['seo'] = [
|
|
90
78
|
{
|
|
91
79
|
type: 'title',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
1
2
|
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
2
3
|
import type { EnduranceUI, EnduranceLocaleContent } from '../index';
|
|
3
4
|
|
|
@@ -29,8 +30,6 @@ const ui: EnduranceUI = {
|
|
|
29
30
|
seaLight: 'Light swell (+15%)',
|
|
30
31
|
seaModerate: 'Moderate swell (+30%)',
|
|
31
32
|
seaStorm: 'Storm (+60%)',
|
|
32
|
-
faqTitle: 'Frequently Asked Questions',
|
|
33
|
-
bibliographyTitle: 'Bibliography',
|
|
34
33
|
};
|
|
35
34
|
const faq: EnduranceLocaleContent['faq'] = [
|
|
36
35
|
{
|
|
@@ -73,16 +72,6 @@ const howTo: EnduranceLocaleContent['howTo'] = [
|
|
|
73
72
|
},
|
|
74
73
|
];
|
|
75
74
|
|
|
76
|
-
const bibliography: EnduranceLocaleContent['bibliography'] = [
|
|
77
|
-
{
|
|
78
|
-
name: 'UK Maritime and Coastguard Agency',
|
|
79
|
-
url: 'https://www.gov.uk/government/organisations/maritime-and-coastguard-agency',
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
name: 'RYA - Royal Yachting Association',
|
|
83
|
-
url: 'https://www.rya.org.uk/',
|
|
84
|
-
},
|
|
85
|
-
];
|
|
86
75
|
const seo: EnduranceLocaleContent['seo'] = [
|
|
87
76
|
{
|
|
88
77
|
type: 'title',
|