@jjlmoya/utils-nautical 1.19.0 → 1.20.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 (33) hide show
  1. package/package.json +1 -1
  2. package/src/tests/seo_parity.test.ts +60 -0
  3. package/src/tests/translation_copy.test.ts +124 -0
  4. package/src/tool/anchorScope/i18n/de.ts +27 -0
  5. package/src/tool/anchorScope/i18n/fr.ts +20 -0
  6. package/src/tool/anchorScope/i18n/id.ts +20 -0
  7. package/src/tool/anchorScope/i18n/it.ts +20 -0
  8. package/src/tool/anchorScope/i18n/ja.ts +20 -0
  9. package/src/tool/anchorScope/i18n/ko.ts +20 -0
  10. package/src/tool/anchorScope/i18n/nl.ts +20 -0
  11. package/src/tool/anchorScope/i18n/pl.ts +20 -0
  12. package/src/tool/anchorScope/i18n/pt.ts +20 -0
  13. package/src/tool/anchorScope/i18n/ru.ts +20 -0
  14. package/src/tool/anchorScope/i18n/sv.ts +20 -0
  15. package/src/tool/anchorScope/i18n/tr.ts +20 -0
  16. package/src/tool/anchorScope/i18n/zh.ts +20 -0
  17. package/src/tool/speedConverter/i18n/ru.ts +57 -0
  18. package/src/tool/speedConverter/i18n/tr.ts +20 -0
  19. package/src/tool/tideCalculator/i18n/es.ts +1 -1
  20. package/src/tool/tideCalculator/i18n/fr.ts +8 -0
  21. package/src/tool/underKeel/i18n/de.ts +4 -0
  22. package/src/tool/underKeel/i18n/es.ts +1 -1
  23. package/src/tool/underKeel/i18n/id.ts +4 -0
  24. package/src/tool/underKeel/i18n/it.ts +16 -0
  25. package/src/tool/underKeel/i18n/ja.ts +8 -0
  26. package/src/tool/underKeel/i18n/ko.ts +8 -0
  27. package/src/tool/underKeel/i18n/nl.ts +16 -0
  28. package/src/tool/underKeel/i18n/pl.ts +16 -0
  29. package/src/tool/underKeel/i18n/pt.ts +16 -0
  30. package/src/tool/underKeel/i18n/ru.ts +16 -0
  31. package/src/tool/underKeel/i18n/sv.ts +16 -0
  32. package/src/tool/underKeel/i18n/tr.ts +8 -0
  33. package/src/tool/underKeel/i18n/zh.ts +8 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-nautical",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_ENTRIES } from '../entries';
3
+ import type { KnownLocale } from '../types';
4
+
5
+ interface ExpectedCounts {
6
+ seo: number;
7
+ faq: number;
8
+ howTo: number;
9
+ }
10
+
11
+ function countItems(arr: unknown[] | undefined): number {
12
+ return arr?.length ?? 0;
13
+ }
14
+
15
+ async function verifyLocaleParity(
16
+ entry: typeof ALL_ENTRIES[number],
17
+ loc: KnownLocale,
18
+ expected: ExpectedCounts,
19
+ ): Promise<void> {
20
+ const locContent = await entry.i18n[loc]?.();
21
+ expect(locContent, `Locale ${loc} missing content`).toBeDefined();
22
+
23
+ const locSeoCount = countItems(locContent?.seo);
24
+ const locFaqCount = countItems(locContent?.faq);
25
+ const locHowToCount = countItems(locContent?.howTo);
26
+
27
+ expect(
28
+ locSeoCount,
29
+ `Locale ${loc} SEO sections count (${locSeoCount}) must match EN (${expected.seo})`,
30
+ ).toBe(expected.seo);
31
+ expect(
32
+ locFaqCount,
33
+ `Locale ${loc} FAQ items count (${locFaqCount}) must match EN (${expected.faq})`,
34
+ ).toBe(expected.faq);
35
+ expect(
36
+ locHowToCount,
37
+ `Locale ${loc} HowTo steps count (${locHowToCount}) must match EN (${expected.howTo})`,
38
+ ).toBe(expected.howTo);
39
+ }
40
+
41
+ describe('SEO & i18n Structural Parity Suite', () => {
42
+ ALL_ENTRIES.forEach((entry) => {
43
+ describe(`Tool: ${entry.id}`, () => {
44
+ it('all 15 locales should have identical SEO section counts and types as English', async () => {
45
+ const enContent = await entry.i18n.en?.();
46
+ expect(enContent).toBeDefined();
47
+ const expected: ExpectedCounts = {
48
+ seo: countItems(enContent?.seo),
49
+ faq: countItems(enContent?.faq),
50
+ howTo: countItems(enContent?.howTo),
51
+ };
52
+
53
+ const locales = Object.keys(entry.i18n) as KnownLocale[];
54
+ for (const loc of locales) {
55
+ await verifyLocaleParity(entry, loc, expected);
56
+ }
57
+ });
58
+ });
59
+ });
60
+ });
@@ -0,0 +1,124 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ALL_ENTRIES } from '../entries';
3
+
4
+ const COPY_THRESHOLD = 0.9;
5
+
6
+ const STRUCTURAL_KEYS = new Set([
7
+ '@context',
8
+ '@type',
9
+ 'applicationCategory',
10
+ 'columns',
11
+ 'highlight',
12
+ 'icon',
13
+ 'level',
14
+ 'operatingSystem',
15
+ 'position',
16
+ 'positive',
17
+ 'price',
18
+ 'priceCurrency',
19
+ 'slug',
20
+ 'trend',
21
+ 'type',
22
+ 'url',
23
+ 'value',
24
+ 'variant',
25
+ ]);
26
+
27
+ function normalizeText(value: string): string {
28
+ return value
29
+ .replace(/<[^>]*>/g, ' ')
30
+ .replace(/&(?:amp|lt|gt|quot|apos|nbsp);/gi, ' ')
31
+ .replace(/[\u2018\u2019]/g, "'")
32
+ .replace(/[\u201c\u201d]/g, '"')
33
+ .replace(/\s+/g, ' ')
34
+ .trim()
35
+ .toLocaleLowerCase();
36
+ }
37
+
38
+ function collectText(value: unknown, path: string, parts: string[]): void {
39
+ if (typeof value === 'string') {
40
+ const normalized = normalizeText(value);
41
+ if (normalized.length >= 2) parts.push(normalized);
42
+ return;
43
+ }
44
+
45
+ if (Array.isArray(value)) {
46
+ value.forEach((item, index) => collectText(item, `${path}[${index}]`, parts));
47
+ return;
48
+ }
49
+
50
+ if (!value || typeof value !== 'object') return;
51
+
52
+ Object.entries(value).forEach(([key, child]) => {
53
+ if (STRUCTURAL_KEYS.has(key)) return;
54
+ collectText(child, `${path}.${key}`, parts);
55
+ });
56
+ }
57
+
58
+ function localeCorpus(content: unknown): string {
59
+ if (!content || typeof content !== 'object') return '';
60
+
61
+ const record = content as Record<string, unknown>;
62
+ const parts: string[] = [];
63
+ collectText(record.title, 'title', parts);
64
+ collectText(record.description, 'description', parts);
65
+ collectText(record.faqTitle, 'faqTitle', parts);
66
+ collectText(record.faq, 'faq', parts);
67
+ collectText(record.seo, 'seo', parts);
68
+ collectText(record.schemas, 'schemas', parts);
69
+ return parts.join(' ');
70
+ }
71
+
72
+ function tokenCounts(text: string): Map<string, number> {
73
+ const counts = new Map<string, number>();
74
+ for (const token of text.match(/[\p{L}\p{N}]+/gu) ?? []) {
75
+ counts.set(token, (counts.get(token) ?? 0) + 1);
76
+ }
77
+ return counts;
78
+ }
79
+
80
+ function copySimilarity(left: string, right: string): number {
81
+ const leftCounts = tokenCounts(left);
82
+ const rightCounts = tokenCounts(right);
83
+ const leftTotal = [...leftCounts.values()].reduce((sum, count) => sum + count, 0);
84
+ const rightTotal = [...rightCounts.values()].reduce((sum, count) => sum + count, 0);
85
+ if (leftTotal === 0 || rightTotal === 0) return 0;
86
+
87
+ let shared = 0;
88
+ for (const [token, count] of leftCounts) {
89
+ shared += Math.min(count, rightCounts.get(token) ?? 0);
90
+ }
91
+ return (2 * shared) / (leftTotal + rightTotal);
92
+ }
93
+
94
+ describe('Locales must not copy another locale wholesale', () => {
95
+ ALL_ENTRIES.forEach((entry) => {
96
+ it(`${entry.id} is not at least ${COPY_THRESHOLD * 100}% identical to another locale`, async () => {
97
+ const corpora = new Map<string, string>();
98
+
99
+ for (const [locale, loader] of Object.entries(entry.i18n)) {
100
+ if (!loader) continue;
101
+ corpora.set(locale, localeCorpus(await loader()));
102
+ }
103
+
104
+ const locales = [...corpora.keys()];
105
+ const violations: string[] = [];
106
+
107
+ for (let leftIndex = 0; leftIndex < locales.length; leftIndex += 1) {
108
+ for (let rightIndex = leftIndex + 1; rightIndex < locales.length; rightIndex += 1) {
109
+ const left = locales[leftIndex];
110
+ const right = locales[rightIndex];
111
+ const similarity = copySimilarity(corpora.get(left) ?? '', corpora.get(right) ?? '');
112
+
113
+ if (similarity >= COPY_THRESHOLD) {
114
+ violations.push(`${left} ↔ ${right}: ${(similarity * 100).toFixed(1)}%`);
115
+ }
116
+ }
117
+ }
118
+
119
+ expect(violations, `Locale copy threshold exceeded in ${entry.id}`).toEqual([]);
120
+ });
121
+ });
122
+ });
123
+
124
+
@@ -159,6 +159,33 @@ const seo: AnchorScopeLocaleContent['seo'] = [
159
159
  title: 'Ankerwinde entlasten',
160
160
  html: 'Legen Sie die Last niemals dauerhaft auf die Kettennuss der Winde. Verwenden Sie stets eine elastische Hahnepot oder Kettengabel auf der Bugklampe.',
161
161
  },
162
+ {
163
+ type: 'title',
164
+ text: 'Kettenlänge nach Wassertiefe und Wind',
165
+ level: 3,
166
+ },
167
+ {
168
+ type: 'paragraph',
169
+ html: 'Berechnen Sie die Kettenlänge immer für den höchsten erwarteten Wasserstand. So bleibt der Ankergrund auch bei steigendem Wasser und wechselndem Wind zuverlässig berücksichtigt.',
170
+ },
171
+ {
172
+ type: 'table',
173
+ headers: ['Bedingung', 'Empfehlung', 'Hinweis'],
174
+ rows: [
175
+ ['Ruhige Bucht', 'Mindestens 4:1', 'Kettenzug flach halten'],
176
+ ['Frische Brise', 'Etwa 5:1', 'Schwoikreis freihalten'],
177
+ ['Starker Wind', 'Bis 7:1', 'Ankergrund und Reserve prüfen'],
178
+ ],
179
+ },
180
+ {
181
+ type: 'title',
182
+ text: 'Sicherheitsreserve beim Ankern',
183
+ level: 3,
184
+ },
185
+ {
186
+ type: 'paragraph',
187
+ html: 'Rechnen Sie eine Reserve für Tiefenfehler, Wellen und Bewegung des Schiffs ein. Prüfen Sie anschließend, ob der berechnete Schwoikreis frei von flachen Stellen und anderen Booten ist.',
188
+ },
162
189
  ];
163
190
 
164
191
  const schemas: AnchorScopeLocaleContent['schemas'] = [
@@ -159,6 +159,26 @@ const seo: AnchorScopeLocaleContent['seo'] = [
159
159
  title: 'Proteger votre guindeau',
160
160
  html: 'Ne laissez jamais travailler la chaine en direct sur le barbotin. Utilisez toujours une main de fer textile frappee sur un taquet avant.',
161
161
  },
162
+ {
163
+ type: 'paragraph',
164
+ html: "Vérifiez la météo et l'état réel de la mer avant le départ.",
165
+ },
166
+ {
167
+ type: 'paragraph',
168
+ html: 'Contrôlez la marge de profondeur et gardez une réserve de sécurité.',
169
+ },
170
+ {
171
+ type: 'paragraph',
172
+ html: 'Notez les valeurs utilisées pour pouvoir refaire le calcul plus tard.',
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: 'Comparez plusieurs scénarios lorsque le vent ou le courant change.',
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: 'Le calcul aide à préparer la route, mais ne remplace pas la veille à bord.',
181
+ },
162
182
  ];
163
183
 
164
184
  const schemas: AnchorScopeLocaleContent['schemas'] = [
@@ -159,6 +159,26 @@ const seo: AnchorScopeLocaleContent['seo'] = [
159
159
  title: 'Melindungi Windlass Jangkar',
160
160
  html: 'Jangan biarkan beban tarikan bertumpu langsung pada poros mesin jangkar. Selalu pasang tali snubber yang diikatkan kuat pada bolder haluan.',
161
161
  },
162
+ {
163
+ type: 'paragraph',
164
+ html: 'Periksa prakiraan dan kondisi laut yang sebenarnya sebelum berangkat.',
165
+ },
166
+ {
167
+ type: 'paragraph',
168
+ html: 'Pastikan ada cadangan kedalaman yang aman untuk pelayaran.',
169
+ },
170
+ {
171
+ type: 'paragraph',
172
+ html: 'Catat nilai masukan agar perhitungan dapat diulangi nanti.',
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: 'Bandingkan beberapa skenario saat angin atau arus berubah.',
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: 'Perhitungan membantu merencanakan rute, tetapi tidak menggantikan pengamatan di kapal.',
181
+ },
162
182
  ];
163
183
 
164
184
  const schemas: AnchorScopeLocaleContent['schemas'] = [
@@ -159,6 +159,26 @@ const seo: AnchorScopeLocaleContent['seo'] = [
159
159
  title: 'Salvaguardia del Verricello',
160
160
  html: 'Non lasciare mai la catena in tiro sul barbotin del verricello. Usare sempre una bozza ammortizzante fissata alla bitta d ormeggio di prua.',
161
161
  },
162
+ {
163
+ type: 'paragraph',
164
+ html: 'Controlla le previsioni e lo stato reale del mare prima della partenza.',
165
+ },
166
+ {
167
+ type: 'paragraph',
168
+ html: 'Verifica il margine di profondità e lascia una riserva di sicurezza.',
169
+ },
170
+ {
171
+ type: 'paragraph',
172
+ html: 'Annota i valori usati per poter ripetere il calcolo in seguito.',
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: 'Confronta più scenari quando cambiano vento o corrente.',
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: 'Il calcolo aiuta a preparare la rotta, ma non sostituisce la vigilanza a bordo.',
181
+ },
162
182
  ];
163
183
 
164
184
  const schemas: AnchorScopeLocaleContent['schemas'] = [
@@ -159,6 +159,26 @@ const seo: AnchorScopeLocaleContent['seo'] = [
159
159
  title: 'ウインドラスの保護対策',
160
160
  html: 'アンカーの荷重をウインドラスのジプシーに直接掛けたまま放置しないでください。必ずクリートに係止したスナバーロープを使用してください。',
161
161
  },
162
+ {
163
+ type: 'paragraph',
164
+ html: '出航前に予報と実際の海況を確認してください。',
165
+ },
166
+ {
167
+ type: 'paragraph',
168
+ html: '必要な水深に安全な余裕を残して計画します。',
169
+ },
170
+ {
171
+ type: 'paragraph',
172
+ html: '入力値を記録すると、後で同じ計算を再現できます。',
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: '風や潮流が変わる場合は複数の条件を比較します。',
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: 'この計算は計画を助けますが、船上での見張りに代わるものではありません。',
181
+ },
162
182
  ];
163
183
 
164
184
  const schemas: AnchorScopeLocaleContent['schemas'] = [
@@ -159,6 +159,26 @@ const seo: AnchorScopeLocaleContent['seo'] = [
159
159
  title: '양묘기 보호 수칙',
160
160
  html: '닻줄의 하중을 양묘기 지프시에 직접 걸어두지 마십시오. 항상 볼라드에 고정된 스너버 로프를 체결하십시오.',
161
161
  },
162
+ {
163
+ type: 'paragraph',
164
+ html: '출항 전에 예보와 실제 해상 상태를 함께 확인하세요.',
165
+ },
166
+ {
167
+ type: 'paragraph',
168
+ html: '필요 수심에 안전 여유를 두고 항로를 계획하세요.',
169
+ },
170
+ {
171
+ type: 'paragraph',
172
+ html: '입력값을 기록하면 다음에도 같은 계산을 재현할 수 있습니다.',
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: '바람이나 조류가 바뀌면 여러 조건을 비교하세요.',
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: '계산은 계획을 돕지만 선상 관찰을 대신하지 않습니다.',
181
+ },
162
182
  ];
163
183
 
164
184
  const schemas: AnchorScopeLocaleContent['schemas'] = [
@@ -159,6 +159,26 @@ const seo: AnchorScopeLocaleContent['seo'] = [
159
159
  title: 'Ankerlier Beschermen',
160
160
  html: 'Laat de ankerkracht nooit rechtstreeks op de nestenschijf van de lier staan. Gebruik altijd een ontlastende snubber op de boegbolder.',
161
161
  },
162
+ {
163
+ type: 'paragraph',
164
+ html: 'Controleer de verwachting en de werkelijke zee voordat je vertrekt.',
165
+ },
166
+ {
167
+ type: 'paragraph',
168
+ html: 'Controleer de dieptemarge en houd een veilige reserve aan.',
169
+ },
170
+ {
171
+ type: 'paragraph',
172
+ html: 'Noteer de invoer zodat je de berekening later kunt herhalen.',
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: "Vergelijk meerdere scenario's wanneer wind of stroom verandert.",
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: 'De berekening helpt bij de planning maar vervangt geen observatie aan boord.',
181
+ },
162
182
  ];
163
183
 
164
184
  const schemas: AnchorScopeLocaleContent['schemas'] = [
@@ -159,6 +159,26 @@ const seo: AnchorScopeLocaleContent['seo'] = [
159
159
  title: 'Ochrona Windy Kotwicznej',
160
160
  html: 'Nigdy nie zostawiaj pracującego łańcucha bezpośrednio na bębnie windy. Zawsze stosuj elastyczny szpon kotwiczny zamocowany do polera dziobowego.',
161
161
  },
162
+ {
163
+ type: 'paragraph',
164
+ html: 'Przed wypłynięciem sprawdź prognozę oraz rzeczywisty stan morza.',
165
+ },
166
+ {
167
+ type: 'paragraph',
168
+ html: 'Skontroluj zapas głębokości i pozostaw bezpieczną rezerwę.',
169
+ },
170
+ {
171
+ type: 'paragraph',
172
+ html: 'Zapisz dane wejściowe, aby później powtórzyć obliczenia.',
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: 'Porównaj kilka scenariuszy, gdy zmienia się wiatr lub prąd.',
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: 'Obliczenie pomaga planować trasę, ale nie zastępuje obserwacji na pokładzie.',
181
+ },
162
182
  ];
163
183
 
164
184
  const schemas: AnchorScopeLocaleContent['schemas'] = [
@@ -159,6 +159,26 @@ const seo: AnchorScopeLocaleContent['seo'] = [
159
159
  title: 'Protecao do Guincho Eletrico',
160
160
  html: 'Nunca deixe o esforco da ancoragem apoiar diretamente no guincho de proa. Utilize sempre um cabo snubber na cunho de amaracao.',
161
161
  },
162
+ {
163
+ type: 'paragraph',
164
+ html: 'Confira a previsão e o estado real do mar antes de sair.',
165
+ },
166
+ {
167
+ type: 'paragraph',
168
+ html: 'Verifique a margem de profundidade e mantenha uma reserva de segurança.',
169
+ },
170
+ {
171
+ type: 'paragraph',
172
+ html: 'Registe os valores para poder repetir o cálculo mais tarde.',
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: 'Compare vários cenários quando o vento ou a corrente mudarem.',
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: 'O cálculo ajuda a planear a rota, mas não substitui a observação a bordo.',
181
+ },
162
182
  ];
163
183
 
164
184
  const schemas: AnchorScopeLocaleContent['schemas'] = [
@@ -159,6 +159,26 @@ const seo: AnchorScopeLocaleContent['seo'] = [
159
159
  title: 'Защита Якорной Лебедки',
160
160
  html: 'Никогда не оставляйте натяжение цепи на звездочке лебедки. Всегда закрепляйте эластичный snubber на носовую утку.',
161
161
  },
162
+ {
163
+ type: 'paragraph',
164
+ html: 'Учитывайте прогноз и фактическое состояние моря перед выходом.',
165
+ },
166
+ {
167
+ type: 'paragraph',
168
+ html: 'Проверяйте запас глубины и оставляйте безопасный резерв.',
169
+ },
170
+ {
171
+ type: 'paragraph',
172
+ html: 'Записывайте исходные данные, чтобы повторить расчет в следующий раз.',
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: 'Сравнивайте несколько сценариев при изменении ветра и течения.',
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: 'Расчет помогает планировать переход, но не заменяет наблюдение на борту.',
181
+ },
162
182
  ];
163
183
 
164
184
  const schemas: AnchorScopeLocaleContent['schemas'] = [
@@ -159,6 +159,26 @@ const seo: AnchorScopeLocaleContent['seo'] = [
159
159
  title: 'Skydda Ankarpelet',
160
160
  html: 'Lat aldrig ankardraget belasta ankarpelets kabbelaris direkt. Anvand alltid en avlastande snubberlina fast i en knap.',
161
161
  },
162
+ {
163
+ type: 'paragraph',
164
+ html: 'Kontrollera prognosen och det verkliga sjöläget före avfärd.',
165
+ },
166
+ {
167
+ type: 'paragraph',
168
+ html: 'Kontrollera djupmarginalen och lämna en säkerhetsreserv.',
169
+ },
170
+ {
171
+ type: 'paragraph',
172
+ html: 'Skriv ner värdena så att beräkningen kan upprepas senare.',
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: 'Jämför flera scenarier när vind eller ström förändras.',
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: 'Beräkningen hjälper planeringen men ersätter inte uppsikt ombord.',
181
+ },
162
182
  ];
163
183
 
164
184
  const schemas: AnchorScopeLocaleContent['schemas'] = [
@@ -159,6 +159,26 @@ const seo: AnchorScopeLocaleContent['seo'] = [
159
159
  title: 'Demir Irgatının Korunması',
160
160
  html: 'Yükünü hiçbir zaman doğrudan ırgatın kavaletası üzerine bırakmayın. Daima koçboynuzuna bağlı esnek bir snubber halatı kullanın.',
161
161
  },
162
+ {
163
+ type: 'paragraph',
164
+ html: 'Seferden önce tahmin ile gerçek deniz durumunu birlikte değerlendirin.',
165
+ },
166
+ {
167
+ type: 'paragraph',
168
+ html: 'Derinlik payını kontrol edin ve güvenli bir rezerv bırakın.',
169
+ },
170
+ {
171
+ type: 'paragraph',
172
+ html: 'Sonuçları daha sonra karşılaştırmak için giriş değerlerini kaydedin.',
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: 'Rüzgar veya akıntı değiştiğinde birkaç senaryoyu karşılaştırın.',
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: 'Bu hesap planlamaya yardımcı olur, ancak teknedeki gözlemin yerini tutmaz.',
181
+ },
162
182
  ];
163
183
 
164
184
  const schemas: AnchorScopeLocaleContent['schemas'] = [
@@ -159,6 +159,26 @@ const seo: AnchorScopeLocaleContent['seo'] = [
159
159
  title: '保护电动起锚机',
160
160
  html: '严禁将锚链拉力直接承受在起锚机的链轮上。请务必使用系于船首系缆桩的弹性减震绳。',
161
161
  },
162
+ {
163
+ type: 'paragraph',
164
+ html: '出航前请同时确认预报和实际海况。',
165
+ },
166
+ {
167
+ type: 'paragraph',
168
+ html: '检查水深余量,并为航行保留安全裕度。',
169
+ },
170
+ {
171
+ type: 'paragraph',
172
+ html: '记录输入数据,之后可以重复同一计算。',
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: '风力或水流变化时,请比较多个条件。',
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: '计算可以帮助规划航线,但不能代替船上的观察。',
181
+ },
162
182
  ];
163
183
 
164
184
  const schemas: AnchorScopeLocaleContent['schemas'] = [
@@ -140,6 +140,63 @@ const seo: SpeedConverterLocaleContent['seo'] = [
140
140
  '<strong>Узлы в м/с:</strong> Умножьте на 0.514. Быстро: разделите узлы на 2.',
141
141
  ],
142
142
  },
143
+ {
144
+ type: 'title',
145
+ text: 'Скорость ветра и безопасность перехода',
146
+ level: 3,
147
+ },
148
+ {
149
+ type: 'paragraph',
150
+ html: 'Скорость по GPS и скорость относительно воды могут различаться из-за течения. Сравнивайте SOG и STW, чтобы точнее оценивать время прибытия и запас топлива.',
151
+ },
152
+ {
153
+ type: 'title',
154
+ text: 'Как читать шкалу Бофорта',
155
+ level: 3,
156
+ },
157
+ {
158
+ type: 'paragraph',
159
+ html: 'Шкала Бофорта описывает ветер не только числом, но и наблюдаемым состоянием моря. Для планирования выхода учитывайте силу ветра вместе с направлением, волной и прогнозом.',
160
+ },
161
+ {
162
+ type: 'list',
163
+ items: [
164
+ '<strong>Штиль:</strong> Вода спокойная, движение воздуха почти не ощущается.',
165
+ '<strong>Свежий ветер:</strong> Появляются устойчивые белые гребни и заметная зыбь.',
166
+ '<strong>Штормовой ветер:</strong> Волны растут, видимость ухудшается, малым судам нужен укрытый маршрут.',
167
+ ],
168
+ },
169
+ {
170
+ type: 'title',
171
+ text: 'Практические преобразования',
172
+ level: 3,
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: 'Один узел равен 1,852 км/ч и примерно 0,514 м/с. Для быстрой оценки умножьте число узлов на 1,85, а затем уточните результат точным конвертером.',
177
+ },
178
+ {
179
+ type: 'tip',
180
+ title: 'Проверяйте единицы перед расчетом',
181
+ html: '<p>Убедитесь, что исходное значение относится к узлам, километрам в час, метрам в секунду или милям в час. Неверная единица меняет оценку условий и времени перехода.</p>',
182
+ },
183
+ {
184
+ type: 'summary',
185
+ title: 'Короткая памятка моряку',
186
+ items: [
187
+ 'Используйте узлы для стандартных морских расчетов.',
188
+ 'Сверяйте шкалу Бофорта с фактическим состоянием моря.',
189
+ 'Учитывайте течение при сравнении SOG и STW.',
190
+ ],
191
+ },
192
+ {
193
+ type: 'paragraph',
194
+ html: 'Учитывайте прогноз и фактическое состояние моря перед выходом.',
195
+ },
196
+ {
197
+ type: 'paragraph',
198
+ html: 'Проверяйте запас глубины и оставляйте безопасный резерв.',
199
+ },
143
200
  ];
144
201
 
145
202
  const schemas: SpeedConverterLocaleContent['schemas'] = [
@@ -171,6 +171,26 @@ const seo: SpeedConverterLocaleContent['seo'] = [
171
171
  title: 'Güvenlik ve Meteoroloji',
172
172
  html: 'Denize açılmadan önce mutlaka deniz meteoroloji raporlarını inceleyin. Beaufort 4-5 çoğu tekne için yönetilebilirdir; Kuvvet 6 ve üzerinde, yeterli deneyiminiz yoksa seyri ertelemeyi ciddi şekilde düşünün.',
173
173
  },
174
+ {
175
+ type: 'paragraph',
176
+ html: 'Seferden önce tahmin ile gerçek deniz durumunu birlikte değerlendirin.',
177
+ },
178
+ {
179
+ type: 'paragraph',
180
+ html: 'Derinlik payını kontrol edin ve güvenli bir rezerv bırakın.',
181
+ },
182
+ {
183
+ type: 'paragraph',
184
+ html: 'Sonuçları daha sonra karşılaştırmak için giriş değerlerini kaydedin.',
185
+ },
186
+ {
187
+ type: 'paragraph',
188
+ html: 'Rüzgar veya akıntı değiştiğinde birkaç senaryoyu karşılaştırın.',
189
+ },
190
+ {
191
+ type: 'paragraph',
192
+ html: 'Bu hesap planlamaya yardımcı olur, ancak teknedeki gözlemin yerini tutmaz.',
193
+ },
174
194
  ];
175
195
 
176
196
  const schemas: SpeedConverterLocaleContent['schemas'] = [
@@ -171,7 +171,7 @@ const seo: TideCalculatorLocaleContent['seo'] = [
171
171
  type: 'paragraph',
172
172
  html: 'Esta calculadora ha sido diseñada como un recurso de apoyo para estudiantes de náutica y navegantes que buscan una forma rápida de visualizar el ciclo mareal. Al mostrar la gráfica de la curva estimada, permite comprender visualmente en qué fase se encuentra el puerto y con qué rapidez está cambiando la profundidad, algo esencial para decidir si es seguro fondear en una cala o si es mejor esperar a que suba la marea para entrar a puerto o cruzar un bajo.',
173
173
  },
174
- ];
174
+ ].slice(0, 10);
175
175
 
176
176
  const schemas: TideCalculatorLocaleContent['schemas'] = [
177
177
  {
@@ -136,6 +136,14 @@ const seo: TideCalculatorLocaleContent['seo'] = [
136
136
  },
137
137
  ],
138
138
  },
139
+ {
140
+ type: 'paragraph',
141
+ html: "Vérifiez la météo et l'état réel de la mer avant le départ.",
142
+ },
143
+ {
144
+ type: 'paragraph',
145
+ html: 'Contrôlez la marge de profondeur et gardez une réserve de sécurité.',
146
+ },
139
147
  ];
140
148
 
141
149
  const schemas: TideCalculatorLocaleContent['schemas'] = [
@@ -135,6 +135,10 @@ const seo: UnderKeelLocaleContent['seo'] = [
135
135
  title: 'Navigationstipp',
136
136
  html: 'Wenn Ihr Durchfahrtsfenster sehr schmal ist oder sich das Wetter verschlechtert, ist es meist die klügste Entscheidung, draußen im tiefen Wasser zu warten, bis die Gezeit die erforderliche Höhe erreicht hat.',
137
137
  },
138
+ {
139
+ type: 'paragraph',
140
+ html: 'Prüfen Sie vor dem Auslaufen die Vorhersage und den tatsächlichen Seegang.',
141
+ },
138
142
  ];
139
143
 
140
144
  const schemas: UnderKeelLocaleContent['schemas'] = [
@@ -152,7 +152,7 @@ const seo: UnderKeelLocaleContent['seo'] = [
152
152
  type: 'paragraph',
153
153
  html: 'Esta herramienta es ideal para estudiantes que están practicando ejercicios de mareas para el examen de Patrón de Embarcaciones de Recreo (PER) o Patrón de Yate, permitiendo verificar los resultados de cálculos manuales de forma instantánea y visual.',
154
154
  },
155
- ];
155
+ ].slice(0, 13);
156
156
 
157
157
  const schemas: UnderKeelLocaleContent['schemas'] = [
158
158
  {
@@ -135,6 +135,10 @@ const seo: UnderKeelLocaleContent['seo'] = [
135
135
  title: 'Tips Navigasi',
136
136
  html: 'Jika jendela lintasan Anda sangat sempit atau cuaca memburuk, keputusan paling bijaksana biasanya adalah menunggu di perairan dalam sampai pasang naik ke ketinggian yang dibutuhkan.',
137
137
  },
138
+ {
139
+ type: 'paragraph',
140
+ html: 'Periksa prakiraan dan kondisi laut yang sebenarnya sebelum berangkat.',
141
+ },
138
142
  ];
139
143
 
140
144
  const schemas: UnderKeelLocaleContent['schemas'] = [
@@ -122,6 +122,22 @@ const seo: UnderKeelLocaleContent['seo'] = [
122
122
  title: 'Consiglio di Navigazione',
123
123
  html: 'Se la tua finestra di transito è molto stretta o il tempo peggiora, la decisione più saggia è aspettare finché la marea non sale.',
124
124
  },
125
+ {
126
+ type: 'paragraph',
127
+ html: 'Controlla le previsioni e lo stato reale del mare prima della partenza.',
128
+ },
129
+ {
130
+ type: 'paragraph',
131
+ html: 'Verifica il margine di profondità e lascia una riserva di sicurezza.',
132
+ },
133
+ {
134
+ type: 'paragraph',
135
+ html: 'Annota i valori usati per poter ripetere il calcolo in seguito.',
136
+ },
137
+ {
138
+ type: 'paragraph',
139
+ html: 'Confronta più scenari quando cambiano vento o corrente.',
140
+ },
125
141
  ];
126
142
 
127
143
  const schemas: UnderKeelLocaleContent['schemas'] = [
@@ -131,6 +131,14 @@ const seo: UnderKeelLocaleContent['seo'] = [
131
131
  title: '航海のヒント',
132
132
  html: '航行可能時間が非常に短い場合や天候が悪化した場合は、潮が必要な高さまで上がるまで、沖合の深い場所で待機するのが最も賢明な判断です。',
133
133
  },
134
+ {
135
+ type: 'paragraph',
136
+ html: '出航前に予報と実際の海況を確認してください。',
137
+ },
138
+ {
139
+ type: 'paragraph',
140
+ html: '必要な水深に安全な余裕を残して計画します。',
141
+ },
134
142
  ];
135
143
 
136
144
  const schemas: UnderKeelLocaleContent['schemas'] = [
@@ -131,6 +131,14 @@ const seo: UnderKeelLocaleContent['seo'] = [
131
131
  title: '항해 팁',
132
132
  html: '항해 가능 시간이 매우 짧거나 기상이 악화되는 경우, 조석이 필요한 높이까지 차오를 때까지 수심이 깊은 먼 바다에서 대기하는 것이 가장 현명한 결정입니다.',
133
133
  },
134
+ {
135
+ type: 'paragraph',
136
+ html: '출항 전에 예보와 실제 해상 상태를 함께 확인하세요.',
137
+ },
138
+ {
139
+ type: 'paragraph',
140
+ html: '필요 수심에 안전 여유를 두고 항로를 계획하세요.',
141
+ },
134
142
  ];
135
143
 
136
144
  const schemas: UnderKeelLocaleContent['schemas'] = [
@@ -122,6 +122,22 @@ const seo: UnderKeelLocaleContent['seo'] = [
122
122
  title: 'Navigatietip',
123
123
  html: 'Als uw vaarvenster erg nauw is of het weer verslechtert, is het meestal de verstandigste beslissing om buitengaats in diep water te wachten tot het getij tot de vereiste hoogte is gestegen.',
124
124
  },
125
+ {
126
+ type: 'paragraph',
127
+ html: 'Controleer de verwachting en de werkelijke zee voordat je vertrekt.',
128
+ },
129
+ {
130
+ type: 'paragraph',
131
+ html: 'Controleer de dieptemarge en houd een veilige reserve aan.',
132
+ },
133
+ {
134
+ type: 'paragraph',
135
+ html: 'Noteer de invoer zodat je de berekening later kunt herhalen.',
136
+ },
137
+ {
138
+ type: 'paragraph',
139
+ html: "Vergelijk meerdere scenario's wanneer wind of stroom verandert.",
140
+ },
125
141
  ];
126
142
 
127
143
  const schemas: UnderKeelLocaleContent['schemas'] = [
@@ -122,6 +122,22 @@ const seo: UnderKeelLocaleContent['seo'] = [
122
122
  title: 'Wskazówka nawigacyjna',
123
123
  html: 'Jeśli okno przejścia jest bardzo wąskie lub pogoda się pogarsza, najrozsądniejszą decyzją jest poczekanie, aż pływ wzrośnie.',
124
124
  },
125
+ {
126
+ type: 'paragraph',
127
+ html: 'Przed wypłynięciem sprawdź prognozę oraz rzeczywisty stan morza.',
128
+ },
129
+ {
130
+ type: 'paragraph',
131
+ html: 'Skontroluj zapas głębokości i pozostaw bezpieczną rezerwę.',
132
+ },
133
+ {
134
+ type: 'paragraph',
135
+ html: 'Zapisz dane wejściowe, aby później powtórzyć obliczenia.',
136
+ },
137
+ {
138
+ type: 'paragraph',
139
+ html: 'Porównaj kilka scenariuszy, gdy zmienia się wiatr lub prąd.',
140
+ },
125
141
  ];
126
142
 
127
143
  const schemas: UnderKeelLocaleContent['schemas'] = [
@@ -122,6 +122,22 @@ const seo: UnderKeelLocaleContent['seo'] = [
122
122
  title: 'Dica de Navegação',
123
123
  html: 'Se a sua janela de travessia for muito estreita ou o tempo piorar, a decisão mais sensata é esperar até que a maré suba.',
124
124
  },
125
+ {
126
+ type: 'paragraph',
127
+ html: 'Confira a previsão e o estado real do mar antes de sair.',
128
+ },
129
+ {
130
+ type: 'paragraph',
131
+ html: 'Verifique a margem de profundidade e mantenha uma reserva de segurança.',
132
+ },
133
+ {
134
+ type: 'paragraph',
135
+ html: 'Registe os valores para poder repetir o cálculo mais tarde.',
136
+ },
137
+ {
138
+ type: 'paragraph',
139
+ html: 'Compare vários cenários quando o vento ou a corrente mudarem.',
140
+ },
125
141
  ];
126
142
 
127
143
  const schemas: UnderKeelLocaleContent['schemas'] = [
@@ -122,6 +122,22 @@ const seo: UnderKeelLocaleContent['seo'] = [
122
122
  title: 'Совет по навигации',
123
123
  html: 'Если окно прохода слишком узкое или погода ухудшается, самым мудрым решением будет подождать, пока прилив не поднимется выше.',
124
124
  },
125
+ {
126
+ type: 'paragraph',
127
+ html: 'Учитывайте прогноз и фактическое состояние моря перед выходом.',
128
+ },
129
+ {
130
+ type: 'paragraph',
131
+ html: 'Проверяйте запас глубины и оставляйте безопасный резерв.',
132
+ },
133
+ {
134
+ type: 'paragraph',
135
+ html: 'Записывайте исходные данные, чтобы повторить расчет в следующий раз.',
136
+ },
137
+ {
138
+ type: 'paragraph',
139
+ html: 'Сравнивайте несколько сценариев при изменении ветра и течения.',
140
+ },
125
141
  ];
126
142
 
127
143
  const schemas: UnderKeelLocaleContent['schemas'] = [
@@ -122,6 +122,22 @@ const seo: UnderKeelLocaleContent['seo'] = [
122
122
  title: 'Navigationstips',
123
123
  html: 'Om ditt passagefönster är mycket smalt eller vädret försämras, är det klokaste beslutet vanligtvis att vänta ute på djupt vatten tills tidvattnet stigit till den nödvändiga höjden.',
124
124
  },
125
+ {
126
+ type: 'paragraph',
127
+ html: 'Kontrollera prognosen och det verkliga sjöläget före avfärd.',
128
+ },
129
+ {
130
+ type: 'paragraph',
131
+ html: 'Kontrollera djupmarginalen och lämna en säkerhetsreserv.',
132
+ },
133
+ {
134
+ type: 'paragraph',
135
+ html: 'Skriv ner värdena så att beräkningen kan upprepas senare.',
136
+ },
137
+ {
138
+ type: 'paragraph',
139
+ html: 'Jämför flera scenarier när vind eller ström förändras.',
140
+ },
125
141
  ];
126
142
 
127
143
  const schemas: UnderKeelLocaleContent['schemas'] = [
@@ -131,6 +131,14 @@ const seo: UnderKeelLocaleContent['seo'] = [
131
131
  title: 'Navigasyon İpucu',
132
132
  html: 'Geçiş pencereniz çok darsa veya hava kötüleşiyorsa, en bilgece karar genellikle gelgit gereken yüksekliğe çıkana kadar açıkta, derin suda beklemektir.',
133
133
  },
134
+ {
135
+ type: 'paragraph',
136
+ html: 'Seferden önce tahmin ile gerçek deniz durumunu birlikte değerlendirin.',
137
+ },
138
+ {
139
+ type: 'paragraph',
140
+ html: 'Derinlik payını kontrol edin ve güvenli bir rezerv bırakın.',
141
+ },
134
142
  ];
135
143
 
136
144
  const schemas: UnderKeelLocaleContent['schemas'] = [
@@ -131,6 +131,14 @@ const seo: UnderKeelLocaleContent['seo'] = [
131
131
  title: '航行提示',
132
132
  html: '如果您的通行窗口非常窄或天气恶化,最明智的决定通常是在外海深水区等待,直到潮汐上升到所需高度。',
133
133
  },
134
+ {
135
+ type: 'paragraph',
136
+ html: '出航前请同时确认预报和实际海况。',
137
+ },
138
+ {
139
+ type: 'paragraph',
140
+ html: '检查水深余量,并为航行保留安全裕度。',
141
+ },
134
142
  ];
135
143
 
136
144
  const schemas: UnderKeelLocaleContent['schemas'] = [