@jjlmoya/utils-nautical 1.16.0 → 1.18.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 (49) hide show
  1. package/package.json +4 -2
  2. package/src/category/i18n/de.ts +1 -1
  3. package/src/category/i18n/en.ts +1 -1
  4. package/src/category/i18n/es.ts +16 -16
  5. package/src/category/i18n/fr.ts +16 -16
  6. package/src/category/i18n/id.ts +1 -1
  7. package/src/category/i18n/it.ts +1 -1
  8. package/src/category/i18n/nl.ts +1 -1
  9. package/src/category/i18n/pl.ts +1 -1
  10. package/src/category/i18n/pt.ts +1 -1
  11. package/src/category/i18n/ru.ts +3 -3
  12. package/src/category/i18n/sv.ts +1 -1
  13. package/src/category/i18n/tr.ts +1 -1
  14. package/src/data.ts +0 -6
  15. package/src/layouts/PreviewLayout.astro +1 -0
  16. package/src/tests/diacritics_density.test.ts +118 -0
  17. package/src/tests/inverted_punctuation.test.ts +84 -0
  18. package/src/tests/no_en_dash.test.ts +70 -0
  19. package/src/tests/pagespeed_best_practices.test.ts +198 -0
  20. package/src/tests/script_density.test.ts +94 -0
  21. package/src/tool/endurance/component.astro +10 -10
  22. package/src/tool/endurance/i18n/es.ts +54 -54
  23. package/src/tool/endurance/i18n/fr.ts +70 -70
  24. package/src/tool/endurance/i18n/ru.ts +9 -9
  25. package/src/tool/nauticalConverter/component.astro +15 -15
  26. package/src/tool/nauticalConverter/i18n/es.ts +46 -46
  27. package/src/tool/nauticalConverter/i18n/fr.ts +53 -53
  28. package/src/tool/nauticalConverter/i18n/pl.ts +3 -3
  29. package/src/tool/nauticalConverter/i18n/ru.ts +8 -8
  30. package/src/tool/nauticalConverter/i18n/zh.ts +3 -3
  31. package/src/tool/sailArea/component.astro +13 -13
  32. package/src/tool/sailArea/i18n/es.ts +53 -53
  33. package/src/tool/sailArea/i18n/fr.ts +70 -70
  34. package/src/tool/sailArea/i18n/ru.ts +8 -8
  35. package/src/tool/sailArea/i18n/zh.ts +5 -5
  36. package/src/tool/speedConverter/i18n/es.ts +58 -58
  37. package/src/tool/speedConverter/i18n/fr.ts +75 -75
  38. package/src/tool/speedConverter/i18n/ru.ts +6 -6
  39. package/src/tool/speedConverter/i18n/zh.ts +5 -5
  40. package/src/tool/tideCalculator/component.astro +5 -5
  41. package/src/tool/tideCalculator/i18n/es.ts +39 -39
  42. package/src/tool/tideCalculator/i18n/fr.ts +43 -43
  43. package/src/tool/tideCalculator/i18n/ru.ts +6 -6
  44. package/src/tool/tideCalculator/i18n/zh.ts +1 -1
  45. package/src/tool/tideCalculator/tide-height-calculator.css +136 -5
  46. package/src/tool/underKeel/component.astro +7 -7
  47. package/src/tool/underKeel/i18n/es.ts +39 -39
  48. package/src/tool/underKeel/i18n/fr.ts +45 -45
  49. package/src/tool/underKeel/i18n/ru.ts +5 -5
@@ -0,0 +1,198 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { readdirSync, readFileSync } from 'fs';
3
+ import { join, relative } from 'path';
4
+ import { ALL_TOOLS } from '../tools';
5
+ import type { SEOSection, ToolLocaleContent } from '../types';
6
+
7
+ const srcDir = join(process.cwd(), 'src');
8
+ const toolDir = join(srcDir, 'tool');
9
+ const geometryReads = [
10
+ 'offsetWidth',
11
+ 'offsetHeight',
12
+ 'offsetTop',
13
+ 'offsetLeft',
14
+ 'clientWidth',
15
+ 'clientHeight',
16
+ 'clientTop',
17
+ 'clientLeft',
18
+ 'scrollWidth',
19
+ 'scrollHeight',
20
+ 'scrollTop',
21
+ 'scrollLeft',
22
+ 'getBoundingClientRect',
23
+ 'getClientRects',
24
+ 'computedStyle',
25
+ 'getComputedStyle',
26
+ ];
27
+ const domWrites = [
28
+ '.style.',
29
+ '.classList.add',
30
+ '.classList.remove',
31
+ '.classList.toggle',
32
+ '.appendChild',
33
+ '.insertBefore',
34
+ '.prepend',
35
+ '.append',
36
+ '.remove',
37
+ '.innerHTML',
38
+ '.textContent',
39
+ '.setAttribute',
40
+ ];
41
+
42
+ function findFiles(dir: string, extensions: string[]): string[] {
43
+ const files: string[] = [];
44
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
45
+ const fullPath = join(dir, entry.name);
46
+ if (entry.isDirectory()) files.push(...findFiles(fullPath, extensions));
47
+ else if (extensions.some((extension) => entry.name.endsWith(extension))) files.push(fullPath);
48
+ }
49
+ return files;
50
+ }
51
+
52
+ function relativePath(file: string): string {
53
+ return relative(process.cwd(), file).replace(/\\/g, '/');
54
+ }
55
+
56
+ function findFormControls(content: string, tagName: 'input' | 'select'): RegExpMatchArray[] {
57
+ return Array.from(content.matchAll(new RegExp(`<${tagName}\\b[^>]*>`, 'gi')));
58
+ }
59
+
60
+ function attrValue(tag: string, attr: string): string | null {
61
+ const match = tag.match(new RegExp(`\\b${attr}\\s*=\\s*(?:"([^"]+)"|'([^']+)'|\\{([^}]+)\\})`, 'i'));
62
+ return match?.[1] ?? match?.[2] ?? match?.[3] ?? null;
63
+ }
64
+
65
+ function booleanAttr(tag: string, attr: string): boolean {
66
+ return new RegExp(`\\b${attr}\\b`, 'i').test(tag);
67
+ }
68
+
69
+ function controlStartIndex(content: string, tag: RegExpMatchArray): number {
70
+ return tag.index ?? content.indexOf(tag[0]);
71
+ }
72
+
73
+ function hasWrappingLabel(content: string, tag: RegExpMatchArray): boolean {
74
+ const index = controlStartIndex(content, tag);
75
+ const before = content.slice(0, index);
76
+ const labelOpen = before.lastIndexOf('<label');
77
+ const labelClose = before.lastIndexOf('</label>');
78
+ const nextLabelClose = content.indexOf('</label>', index + tag[0].length);
79
+ return labelOpen > labelClose && nextLabelClose !== -1;
80
+ }
81
+
82
+ function hasAccessibleName(content: string, tag: RegExpMatchArray): boolean {
83
+ const source = tag[0];
84
+ if (attrValue(source, 'aria-label')) return true;
85
+ if (attrValue(source, 'aria-labelledby')) return true;
86
+ const id = attrValue(source, 'id');
87
+ if (id && hasExplicitLabel(content, id)) return true;
88
+ return hasWrappingLabel(content, tag);
89
+ }
90
+
91
+ function isVisuallyHiddenFileInput(tag: string): boolean {
92
+ const type = attrValue(tag, 'type')?.toLowerCase() ?? 'text';
93
+ const attributes = `${attrValue(tag, 'style') ?? ''} ${attrValue(tag, 'class') ?? ''}`.toLowerCase();
94
+ const hiddenPatterns = ['display:none', 'display: none', 'file-input'];
95
+ return type === 'file' && hiddenPatterns.some((pattern) => attributes.includes(pattern));
96
+ }
97
+
98
+ function isIgnoredInput(tag: string): boolean {
99
+ const type = attrValue(tag, 'type')?.toLowerCase() ?? 'text';
100
+ return ['hidden', 'button', 'submit', 'reset'].includes(type) || booleanAttr(tag, 'aria-hidden') || isVisuallyHiddenFileInput(tag);
101
+ }
102
+
103
+ function controlFailures(content: string, tagName: 'input' | 'select'): string[] {
104
+ return findFormControls(content, tagName)
105
+ .filter((tag) => tagName !== 'input' || !isIgnoredInput(tag[0]))
106
+ .filter((tag) => !hasAccessibleName(content, tag))
107
+ .map((tag) => tag[0]);
108
+ }
109
+
110
+ function explicitLabelMessage(tagName: string, path: string, failures: string[]): string {
111
+ return `${tagName} controls without label, wrapping label, aria-label or aria-labelledby in ${path}:\n${failures.join('\n')}`;
112
+ }
113
+
114
+ function hasExplicitLabel(content: string, id: string): boolean {
115
+ const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
116
+ return (
117
+ new RegExp(`<label\\b[^>]*\\bfor\\s*=\\s*["']${escapedId}["'][^>]*>`, 'i').test(content)
118
+ || new RegExp(`<label\\b[^>]*\\bfor\\s*=\\s*\\{${escapedId}\\}[^>]*>`, 'i').test(content)
119
+ );
120
+ }
121
+
122
+ function headingLevels(sections: SEOSection[]): number[] {
123
+ return sections
124
+ .filter((section) => section.type === 'title')
125
+ .map((section) => Number('level' in section ? section.level : 0))
126
+ .filter((level) => Number.isInteger(level) && level > 0);
127
+ }
128
+
129
+ function findHeadingLevelJumps(levels: number[]): string[] {
130
+ const failures: string[] = [];
131
+ levels.forEach((level, index) => {
132
+ const previous = index === 0 ? 1 : levels[index - 1];
133
+ if (previous && level > previous + 1) {
134
+ failures.push(`h${previous} -> h${level}`);
135
+ }
136
+ });
137
+ return failures;
138
+ }
139
+
140
+ function hasDomWriteBeforeGeometryRead(content: string): boolean {
141
+ const normalized = content.replace(/\s+/g, ' ');
142
+ return domWrites.some((write) => {
143
+ const writeIndex = normalized.indexOf(write);
144
+ if (writeIndex === -1) return false;
145
+ return geometryReads.some((read) => normalized.indexOf(read, writeIndex + write.length) !== -1);
146
+ });
147
+ }
148
+
149
+ describe('PageSpeed best-practice guards', () => {
150
+ const astroToolFiles = findFiles(toolDir, ['.astro']);
151
+ const scriptFiles = findFiles(toolDir, ['.astro', '.ts', '.js']);
152
+
153
+ astroToolFiles.forEach((file) => {
154
+ const displayPath = relativePath(file);
155
+
156
+ it(`${displayPath} labels every input with an explicit label`, () => {
157
+ const content = readFileSync(file, 'utf-8');
158
+ const failures = controlFailures(content, 'input');
159
+
160
+ expect(failures, explicitLabelMessage('Input', displayPath, failures)).toEqual([]);
161
+ });
162
+
163
+ it(`${displayPath} labels every select with an explicit label`, () => {
164
+ const content = readFileSync(file, 'utf-8');
165
+ const failures = controlFailures(content, 'select');
166
+
167
+ expect(failures, explicitLabelMessage('Select', displayPath, failures)).toEqual([]);
168
+ });
169
+ });
170
+
171
+ ALL_TOOLS.forEach((tool) => {
172
+ Object.entries(tool.entry.i18n).forEach(([locale, loader]) => {
173
+ it(`${tool.entry.id}/${locale} keeps SEO headings sequential`, async () => {
174
+ if (!loader) return;
175
+ const content = (await loader()) as ToolLocaleContent;
176
+ const levels = headingLevels(content.seo);
177
+ const failures = findHeadingLevelJumps(levels);
178
+
179
+ expect(
180
+ failures,
181
+ `SEO headings in ${tool.entry.id}/${locale} skip levels: ${failures.join(', ')}`,
182
+ ).toEqual([]);
183
+ });
184
+ });
185
+ });
186
+
187
+ scriptFiles.forEach((file) => {
188
+ const displayPath = relativePath(file);
189
+
190
+ it(`${displayPath} avoids static forced-reflow patterns`, () => {
191
+ const content = readFileSync(file, 'utf-8');
192
+ expect(
193
+ hasDomWriteBeforeGeometryRead(content),
194
+ `${displayPath} appears to read layout geometry after DOM/style mutations. Split writes and reads across frames or measure before mutating.`,
195
+ ).toBe(false);
196
+ });
197
+ });
198
+ });
@@ -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
+ });
@@ -12,10 +12,10 @@ const { ui } = Astro.props;
12
12
  <div class="ec-main-container">
13
13
  <aside class="ec-sidebar-inputs">
14
14
  <div class="ec-compact-group">
15
- <label>{ui.tankCapacityLabel}</label>
15
+ <span class="ec-group-label">{ui.tankCapacityLabel}</span>
16
16
  <div class="ec-dual-row">
17
17
  <div>
18
- <span class="ec-sub-label">{ui.mainTankLabel}</span>
18
+ <label for="tank1" class="ec-sub-label">{ui.mainTankLabel}</label>
19
19
  <div class="ec-field-wrapper">
20
20
  <span class="ec-field-icon icon-fuel"></span>
21
21
  <input type="number" id="tank1" class="ec-compact-input" value="200" min="0" step="10" />
@@ -23,7 +23,7 @@ const { ui } = Astro.props;
23
23
  </div>
24
24
  </div>
25
25
  <div>
26
- <span class="ec-sub-label">{ui.auxTankLabel}</span>
26
+ <label for="tank2" class="ec-sub-label">{ui.auxTankLabel}</label>
27
27
  <div class="ec-field-wrapper">
28
28
  <span class="ec-field-icon icon-fuel"></span>
29
29
  <input type="number" id="tank2" class="ec-compact-input" value="0" min="0" step="10" />
@@ -34,7 +34,7 @@ const { ui } = Astro.props;
34
34
  </div>
35
35
 
36
36
  <div class="ec-compact-group">
37
- <label>{ui.currentFuelLabel}</label>
37
+ <label for="f-current">{ui.currentFuelLabel}</label>
38
38
  <div class="ec-field-wrapper">
39
39
  <span class="ec-field-icon icon-bucket"></span>
40
40
  <input type="number" id="f-current" class="ec-compact-input" value="150" min="0" step="5" />
@@ -43,7 +43,7 @@ const { ui } = Astro.props;
43
43
  </div>
44
44
 
45
45
  <div class="ec-compact-group">
46
- <label>{ui.seaConditionsLabel}</label>
46
+ <label for="sea-s">{ui.seaConditionsLabel}</label>
47
47
  <div class="ec-field-wrapper">
48
48
  <span class="ec-field-icon icon-wave"></span>
49
49
  <select id="sea-s" class="ec-compact-select">
@@ -56,7 +56,7 @@ const { ui } = Astro.props;
56
56
  </div>
57
57
 
58
58
  <div class="ec-compact-group">
59
- <label>{ui.consumptionLabel}</label>
59
+ <label for="f-cons">{ui.consumptionLabel}</label>
60
60
  <div class="ec-field-wrapper">
61
61
  <span class="ec-field-icon icon-fire"></span>
62
62
  <input type="number" id="f-cons" class="ec-compact-input" value="25" min="0.1" step="1" />
@@ -65,7 +65,7 @@ const { ui } = Astro.props;
65
65
  </div>
66
66
 
67
67
  <div class="ec-compact-group">
68
- <label>{ui.cruiseSpeedLabel}</label>
68
+ <label for="v-cruiser">{ui.cruiseSpeedLabel}</label>
69
69
  <div class="ec-field-wrapper">
70
70
  <span class="ec-field-icon icon-wind"></span>
71
71
  <input type="number" id="v-cruiser" class="ec-compact-input" value="8" min="0.1" step="0.5" />
@@ -74,7 +74,7 @@ const { ui } = Astro.props;
74
74
  </div>
75
75
 
76
76
  <div class="ec-compact-group">
77
- <label>{ui.reserveLabel}</label>
77
+ <label for="f-res">{ui.reserveLabel}</label>
78
78
  <div class="ec-field-wrapper">
79
79
  <span class="ec-field-icon icon-shield"></span>
80
80
  <input type="number" id="f-res" class="ec-compact-input" value="20" min="0" max="50" step="5" />
@@ -83,7 +83,7 @@ const { ui } = Astro.props;
83
83
  </div>
84
84
 
85
85
  <div class="ec-compact-group">
86
- <label>{ui.fuelPriceLabel}</label>
86
+ <label for="f-price">{ui.fuelPriceLabel}</label>
87
87
  <div class="ec-field-wrapper">
88
88
  <span class="ec-field-icon">€</span>
89
89
  <input type="number" id="f-price" class="ec-compact-input" value="1.8" min="0" step="0.05" />
@@ -137,7 +137,7 @@ const { ui } = Astro.props;
137
137
  <div class="ec-inverter-header">{ui.inverseCalcLabel}</div>
138
138
  <div class="ec-inverter-row">
139
139
  <div>
140
- <div class="ec-inverter-label">{ui.desiredDistLabel}</div>
140
+ <label for="inv-dist" class="ec-inverter-label">{ui.desiredDistLabel}</label>
141
141
  <div class="ec-field-wrapper">
142
142
  <input type="number" id="inv-dist" class="ec-compact-input" value="50" min="0" step="5" />
143
143
  <span class="ec-unit-tag">MN</span>
@@ -3,29 +3,29 @@ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dt
3
3
  import type { EnduranceUI, EnduranceLocaleContent } from '../index';
4
4
 
5
5
  const slug = 'calculadora-autonomia-nautica';
6
- const title = 'Calculadora de Autonomia Nautica';
6
+ const title = 'Calculadora de Autonomía Náutica';
7
7
  const description =
8
- 'Calcula tu alcance maximo y distancia segura segun consumo, capacidad y velocidad de crucero. Gestion de combustible para embarcaciones a motor.';
8
+ 'Calcula tu alcance máximo y distancia segura según consumo, capacidad y velocidad de crucero. Gestión de combustible para embarcaciones a motor.';
9
9
 
10
10
  const ui: EnduranceUI = {
11
- tankCapacityLabel: 'Capacidad del Deposito',
11
+ tankCapacityLabel: 'Capacidad del Depósito',
12
12
  mainTankLabel: 'Ppal',
13
13
  auxTankLabel: 'Aux',
14
14
  currentFuelLabel: 'Combustible Actual',
15
15
  seaConditionsLabel: 'Condiciones Mar/Viento',
16
- consumptionLabel: 'Consumo Teorico',
16
+ consumptionLabel: 'Consumo Teórico',
17
17
  cruiseSpeedLabel: 'Velocidad Crucero',
18
18
  reserveLabel: 'Reserva',
19
19
  fuelPriceLabel: 'Precio Litro',
20
- maxRangeLabel: 'Alcance Maximo',
20
+ maxRangeLabel: 'Alcance Máximo',
21
21
  realPerformanceLabel: 'Rendimiento Real',
22
- hoursLabel: 'Autonomia Horas',
22
+ hoursLabel: 'Autonomía Horas',
23
23
  safeMilesLabel: 'Millas Seguras',
24
24
  tankValueLabel: 'Valor Tanque',
25
- inverseCalcLabel: 'Calculo Inverso: ¿Cuanto necesito repostar?',
25
+ inverseCalcLabel: 'Calculo Inverso: ¿Cuánto necesito repostar?',
26
26
  desiredDistLabel: 'Distancia Deseada',
27
- minFuelLabel: 'Combustible Minimo Requerido',
28
- warningLabel: 'Recordatorio: Una reserva del 20% es el minimo absoluto recomendado por seguridad nautica.',
27
+ minFuelLabel: 'Combustible Mínimo Requerido',
28
+ warningLabel: 'Recordatorio: Una reserva del 20% es el mínimo absoluto recomendado por seguridad náutica.',
29
29
  seaCalm: 'Calma (1.0x)',
30
30
  seaLight: 'Marejadilla (+15%)',
31
31
  seaModerate: 'Marejada (+30%)',
@@ -34,59 +34,59 @@ const ui: EnduranceUI = {
34
34
 
35
35
  const faq: EnduranceLocaleContent['faq'] = [
36
36
  {
37
- question: '¿Como se calcula la autonomia de una embarcacion a motor?',
38
- answer: 'La autonomia se calcula dividiendo el combustible disponible entre el consumo horario para obtener las horas totales, y multiplicando ese tiempo por la velocidad de crucero. La herramienta aplica ademas un factor de mar para reflejar condiciones reales de navegacion.',
37
+ question: '¿Cómo se calcula la autonomía de una embarcación a motor?',
38
+ answer: 'La autonomía se calcula dividiendo el combustible disponible entre el consumo horario para obtener las horas totales, y multiplicando ese tiempo por la velocidad de crucero. La herramienta aplica además un factor de mar para reflejar condiciones reales de navegación.',
39
39
  },
40
40
  {
41
- question: '¿Por que se recomienda una reserva del 20% en nautica?',
42
- answer: 'La reserva del 20% es un estandar de seguridad nautica que garantiza combustible suficiente ante imprevistos: corrientes contrarias, rodeos por mal tiempo, fallos en las previsiones de consumo o necesidad de asistir a otra embarcacion. Muchas aseguradoras y capitanias lo consideran obligatorio.',
41
+ question: '¿Por qué se recomienda una reserva del 20% en náutica?',
42
+ answer: 'La reserva del 20% es un estándar de seguridad náutica que garantiza combustible suficiente ante imprevistos: corrientes contrarias, rodeos por mal tiempo, fallos en las previsiones de consumo o necesidad de asistir a otra embarcación. Muchas aseguradoras y capitanías lo consideran obligatorio.',
43
43
  },
44
44
  {
45
- question: '¿Que es el rendimiento en L/MN y como afecta a la planificacion?',
46
- answer: 'El rendimiento en litros por milla nautica (L/MN) es la eficiencia real del motor en condiciones de mar. A diferencia del consumo horario, permite calcular exactamente cuanto combustible necesitas para recorrer una distancia concreta, independientemente de tu velocidad.',
45
+ question: '¿Qué es el rendimiento en L/MN y cómo afecta a la planificación?',
46
+ answer: 'El rendimiento en litros por milla náutica (L/MN) es la eficiencia real del motor en condiciones de mar. A diferencia del consumo horario, permite calcular exactamente cuánto combustible necesitas para recorrer una distancia concreta, independientemente de tu velocidad.',
47
47
  },
48
48
  {
49
- question: '¿Como influye el estado del mar en el consumo real?',
50
- answer: 'El estado del mar incrementa el consumo porque el casco trabaja mas contra la resistencia del agua y el viento. En marejadilla el consumo aumenta un 15%, en marejada un 30% y en temporal puede dispararse un 60% o mas. Ignorar este factor puede dejar una embarcacion sin combustible antes de llegar a puerto.',
49
+ question: '¿Cómo influye el estado del mar en el consumo real?',
50
+ answer: 'El estado del mar incrementa el consumo porque el casco trabaja más contra la resistencia del agua y el viento. En marejadilla el consumo aumenta un 15%, en marejada un 30% y en temporal puede dispararse un 60% o más. Ignorar este factor puede dejar una embarcación sin combustible antes de llegar a puerto.',
51
51
  },
52
52
  ];
53
53
 
54
54
  const howTo: EnduranceLocaleContent['howTo'] = [
55
55
  {
56
- name: 'Introduce la capacidad de tus depositos',
57
- text: 'Indica los litros del deposito principal y del auxiliar si tienes uno. La herramienta calculara el porcentaje de llenado actual.',
56
+ name: 'Introduce la capacidad de tus depósitos',
57
+ text: 'Indica los litros del depósito principal y del auxiliar si tienes uno. La herramienta calculará el porcentaje de llenado actual.',
58
58
  },
59
59
  {
60
60
  name: 'Indica el combustible actual',
61
- text: 'Introduce los litros reales que tienes en este momento. Puedes medirlos con el nivel del barco o estimarlos segun el ultimo repostaje.',
61
+ text: 'Introduce los litros reales que tienes en este momento. Puedes medirlos con el nivel del barco o estimarlos según el último repostaje.',
62
62
  },
63
63
  {
64
- name: 'Ajusta las condiciones de navegacion',
65
- text: 'Selecciona el estado del mar previsto para tu travesia. El factor de mar corrije el consumo teorico al consumo real esperado.',
64
+ name: 'Ajusta las condiciones de navegación',
65
+ text: 'Selecciona el estado del mar previsto para tu travesía. El factor de mar corrige el consumo teórico al consumo real esperado.',
66
66
  },
67
67
  {
68
68
  name: 'Introduce consumo y velocidad de crucero',
69
- text: 'Usa los datos del fabricante o tus propios registros de consumo horario y la velocidad a la que navegaras habitualmente.',
69
+ text: 'Usa los datos del fabricante o tus propios registros de consumo horario y la velocidad a la que navegarás habitualmente.',
70
70
  },
71
71
  {
72
- name: 'Revisa la distancia segura y el calculo inverso',
73
- text: 'La herramienta te muestra cuantas millas puedes recorrer con seguridad y cuanto combustible necesitas para llegar a un destino concreto.',
72
+ name: 'Revisa la distancia segura y el cálculo inverso',
73
+ text: 'La herramienta te muestra cuántas millas puedes recorrer con seguridad y cuánto combustible necesitas para llegar a un destino concreto.',
74
74
  },
75
75
  ];
76
76
 
77
77
  const seo: EnduranceLocaleContent['seo'] = [
78
78
  {
79
79
  type: 'title',
80
- text: 'Gestion de Combustible y Autonomia en Navegacion a Motor',
80
+ text: 'Gestión de Combustible y Autonomía en Navegación a Motor',
81
81
  level: 2,
82
82
  },
83
83
  {
84
84
  type: 'paragraph',
85
- html: 'La <strong>autonomia nautica</strong> es uno de los calculos mas criticos antes de zarpar en una embarcacion a motor. Conocer con precision cuantas millas puedes recorrer con el combustible disponible es la diferencia entre una travesia planificada y una emergencia en alta mar.',
85
+ html: 'La <strong>autonomía náutica</strong> es uno de los cálculos más críticos antes de zarpar en una embarcación a motor. Conocer con precisión cuántas millas puedes recorrer con el combustible disponible es la diferencia entre una travesía planificada y una emergencia en alta mar.',
86
86
  },
87
87
  {
88
88
  type: 'paragraph',
89
- html: 'Esta calculadora de autonomia integra el <strong>factor de condiciones de mar</strong>, el porcentaje de reserva de seguridad y el calculo inverso de combustible necesario para que la gestion de combustible sea completa y fiable en cualquier situacion.',
89
+ html: 'Esta calculadora de autonomía integra el <strong>factor de condiciones de mar</strong>, el porcentaje de reserva de seguridad y el cálculo inverso de combustible necesario para que la gestión de combustible sea completa y fiable en cualquier situación.',
90
90
  },
91
91
  {
92
92
  type: 'title',
@@ -95,19 +95,19 @@ const seo: EnduranceLocaleContent['seo'] = [
95
95
  },
96
96
  {
97
97
  type: 'paragraph',
98
- html: 'La regla clasica de gestion de combustible en nautica divide el deposito en tres partes iguales para garantizar siempre un margen de seguridad:',
98
+ html: 'La regla clásica de gestión de combustible en náutica divide el depósito en tres partes iguales para garantizar siempre un margen de seguridad:',
99
99
  },
100
100
  {
101
101
  type: 'list',
102
102
  items: [
103
103
  '<strong>Un tercio para la ida:</strong> El combustible necesario para llegar al destino previsto.',
104
- '<strong>Un tercio para la vuelta:</strong> El combustible de regreso al puerto de salida o al mas cercano.',
104
+ '<strong>Un tercio para la vuelta:</strong> El combustible de regreso al puerto de salida o al más cercano.',
105
105
  '<strong>Un tercio de reserva:</strong> El margen de seguridad ante imprevistos, corrientes o condiciones adversas.',
106
106
  ],
107
107
  },
108
108
  {
109
109
  type: 'paragraph',
110
- html: 'En la calculadora puedes configurar el porcentaje de reserva segun tu criterio, aunque <strong>nunca se recomienda bajar del 20%</strong>. Con depositos mas grandes o travesias largas, muchos patrones experimentados elevan este margen al 30% o incluso al 33%.',
110
+ html: 'En la calculadora puedes configurar el porcentaje de reserva según tu criterio, aunque <strong>nunca se recomienda bajar del 20%</strong>. Con depósitos más grandes o travesías largas, muchos patrones experimentados elevan este margen al 30% o incluso al 33%.',
111
111
  },
112
112
  {
113
113
  type: 'title',
@@ -116,30 +116,30 @@ const seo: EnduranceLocaleContent['seo'] = [
116
116
  },
117
117
  {
118
118
  type: 'paragraph',
119
- html: 'El consumo teorico del fabricante se calcula en condiciones ideales de laboratorio. En la practica, multiples factores alteran el <strong>consumo real</strong> de combustible. La velocidad de casco es el mas determinante: navegar a velocidades superiores a la velocidad economica puede duplicar o triplicar el consumo por milla recorrida.',
119
+ html: 'El consumo teórico del fabricante se calcula en condiciones ideales de laboratorio. En la práctica, múltiples factores alteran el <strong>consumo real</strong> de combustible. La velocidad de casco es el más determinante: navegar a velocidades superiores a la velocidad económica puede duplicar o triplicar el consumo por milla recorrida.',
120
120
  },
121
121
  {
122
122
  type: 'table',
123
- headers: ['Estado de la Navegacion', 'Efecto en la Autonomia', 'Consejo'],
123
+ headers: ['Estado de la Navegación', 'Efecto en la Autonomía', 'Consejo'],
124
124
  rows: [
125
- ['<strong>Casco con Incrustaciones</strong>', 'Reduce la autonomia hasta un 20%', 'Limpia el casco antes de temporada'],
125
+ ['<strong>Casco con Incrustaciones</strong>', 'Reduce la autonomía hasta un 20%', 'Limpia el casco antes de temporada'],
126
126
  ['<strong>Corriente en Contra</strong>', 'Puede reducir la velocidad efectiva un 30%', 'Calcula siempre con la velocidad real sobre el fondo'],
127
- ['<strong>Viento de Proa</strong>', 'Aumenta la resistencia y el consumo un 10-25%', 'Usa el factor de marejada o marejada para el calculo'],
127
+ ['<strong>Viento de Proa</strong>', 'Aumenta la resistencia y el consumo un 10-25%', 'Usa el factor de marejada o marejada para el cálculo'],
128
128
  ['<strong>Exceso de Carga</strong>', 'Aumenta el calado y la resistencia', 'Pesa el equipo y provisiones antes de calcular'],
129
129
  ],
130
130
  },
131
131
  {
132
132
  type: 'title',
133
- text: 'Glosario Tecnica de Autonomia',
133
+ text: 'Glosario Técnico de Autonomía',
134
134
  level: 3,
135
135
  },
136
136
  {
137
137
  type: 'glossary',
138
138
  items: [
139
- { term: 'Rendimiento (L/MN)', definition: 'Litros de combustible consumidos por cada milla nautica recorrida. Es el indicador mas util para planificar travesias porque relaciona consumo con distancia real.' },
140
- { term: 'Velocidad de Crucero', definition: 'La velocidad optima a la que el motor ofrece el mejor equilibrio entre velocidad y consumo. Generalmente entre el 70% y el 80% de la potencia maxima.' },
141
- { term: 'Reserva Critica', definition: 'El porcentaje del deposito que nunca debe consumirse. Se recomienda un minimo del 20% para cubrir imprevistos y evitar que entre aire en el circuito de combustible.' },
142
- { term: 'Consumo Especifico', definition: 'Gramos de combustible por kilovatio-hora de potencia producida (g/kWh). Medida tecnica del motor que permite comparar la eficiencia entre distintos propulsores.' },
139
+ { term: 'Rendimiento (L/MN)', definition: 'Litros de combustible consumidos por cada milla náutica recorrida. Es el indicador más útil para planificar travesías porque relaciona consumo con distancia real.' },
140
+ { term: 'Velocidad de Crucero', definition: 'La velocidad óptima a la que el motor ofrece el mejor equilibrio entre velocidad y consumo. Generalmente entre el 70% y el 80% de la potencia máxima.' },
141
+ { term: 'Reserva Crítica', definition: 'El porcentaje del depósito que nunca debe consumirse. Se recomienda un mínimo del 20% para cubrir imprevistos y evitar que entre aire en el circuito de combustible.' },
142
+ { term: 'Consumo Específico', definition: 'Gramos de combustible por kilovatio-hora de potencia producida (g/kWh). Medida técnica del motor que permite comparar la eficiencia entre distintos propulsores.' },
143
143
  ],
144
144
  },
145
145
  {
@@ -149,48 +149,48 @@ const seo: EnduranceLocaleContent['seo'] = [
149
149
  },
150
150
  {
151
151
  type: 'paragraph',
152
- html: 'Una fuente habitual de problemas en embarcaciones con poco combustible son los <strong>lodos del fondo del deposito</strong>. Cuando el nivel baja mucho, los sedimentos acumulados durante anos pueden llegar al filtro y taponarlo, dejando el motor sin combustible aunque el nivel marque algunos litros.',
152
+ html: 'Una fuente habitual de problemas en embarcaciones con poco combustible son los <strong>lodos del fondo del depósito</strong>. Cuando el nivel baja mucho, los sedimentos acumulados durante años pueden llegar al filtro y taponarlo, dejando el motor sin combustible aunque el nivel marque algunos litros.',
153
153
  },
154
154
  {
155
155
  type: 'paragraph',
156
- html: 'El movimiento del barco en mar con poco combustible tambien puede causar problemas: el liquido oscila con las olas y puede dejar momentaneamente sin alimentacion a la bomba. Mantener la reserva garantiza que esto no ocurra en un momento critico como una maniobra de entrada a puerto.',
156
+ html: 'El movimiento del barco en mar con poco combustible también puede causar problemas: el líquido oscila con las olas y puede dejar momentáneamente sin alimentación a la bomba. Mantener la reserva garantiza que esto no ocurra en un momento crítico como una maniobra de entrada a puerto.',
157
157
  },
158
158
  {
159
159
  type: 'tip',
160
160
  title: 'Consejo de Ahorro',
161
- html: 'Reducir la velocidad entre un 10% y un 15% respecto a la de crucero habitual puede mejorar la eficiencia hasta un 30%. Si tienes tiempo y el tiempo acompana, navegar mas despacio es siempre la decision mas economica y segura.',
161
+ html: 'Reducir la velocidad entre un 10% y un 15% respecto a la de crucero habitual puede mejorar la eficiencia hasta un 30%. Si tienes tiempo y el tiempo acompaña, navegar más despacio es siempre la decisión más económica y segura.',
162
162
  },
163
163
  {
164
164
  type: 'title',
165
- text: 'Diferencia entre Millas Náuticas (MN) y Kilometros',
165
+ text: 'Diferencia entre Millas Náuticas (MN) y Kilómetros',
166
166
  level: 3,
167
167
  },
168
168
  {
169
169
  type: 'paragraph',
170
- html: 'Una <strong>milla nautica</strong> equivale a 1.852 metros (1,852 km), y es la unidad de distancia universal en navegacion maritima y aerea. Esta basada en el arco de un minuto de grado geografico, lo que la hace ideal para la navegacion con cartas en coordenadas geograficas. La velocidad en millasnáuticas por hora se denomina nudo (kn).',
170
+ html: 'Una <strong>milla náutica</strong> equivale a 1.852 metros (1,852 km), y es la unidad de distancia universal en navegación marítima y aérea. Está basada en el arco de un minuto de grado geográfico, lo que la hace ideal para la navegación con cartas en coordenadas geográficas. La velocidad en millas náuticas por hora se denomina nudo (kn).',
171
171
  },
172
172
  {
173
173
  type: 'comparative',
174
174
  items: [
175
175
  {
176
- title: 'Indicador Analogico de Aguja',
176
+ title: 'Indicador Analógico de Aguja',
177
177
  description: 'Sistema tradicional de flotador',
178
178
  points: [
179
- 'Bajo coste y sencillez de instalacion',
179
+ 'Bajo coste y sencillez de instalación',
180
180
  'No requiere electricidad para funcionar',
181
- 'Imprecision en mar movido por el movimiento del combustible',
182
- 'No muestra consumo ni autonomia estimada',
181
+ 'Imprecisión en mar movido por el movimiento del combustible',
182
+ 'No muestra consumo ni autonomía estimada',
183
183
  ],
184
184
  },
185
185
  {
186
186
  title: 'Calculador de Flujo Digital',
187
- description: 'Sensores electronicos de caudal',
187
+ description: 'Sensores electrónicos de caudal',
188
188
  highlight: true,
189
189
  points: [
190
- 'Precision del 1-2% en el consumo real',
191
- 'Muestra autonomia, consumo y coste en tiempo real',
190
+ 'Précisión del 1-2% en el consumo real',
191
+ 'Muestra autonomía, consumo y coste en tiempo real',
192
192
  'Integrable con chartplotters y NMEA 2000',
193
- 'Requiere calibracion inicial y mantenimiento',
193
+ 'Requiere calibración inicial y mantenimiento',
194
194
  ],
195
195
  },
196
196
  ],
@@ -219,7 +219,7 @@ const schemas: EnduranceLocaleContent['schemas'] = [
219
219
  {
220
220
  '@context': 'https://schema.org',
221
221
  '@type': 'HowTo',
222
- name: `Como usar: ${title}`,
222
+ name: `Cómo usar: ${title}`,
223
223
  step: howTo.map((s) => ({ '@type': 'HowToStep', name: s.name, text: s.text })),
224
224
  } as WithContext<HowTo>,
225
225
  ];