@jjlmoya/utils-travel 1.14.0 → 1.16.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 +5 -3
- package/src/category/i18n/de.ts +3 -3
- package/src/category/i18n/en.ts +1 -1
- package/src/category/i18n/es.ts +1 -1
- package/src/category/i18n/fr.ts +15 -15
- package/src/category/i18n/pl.ts +1 -1
- package/src/category/i18n/ru.ts +4 -4
- package/src/category/i18n/sv.ts +1 -1
- package/src/data.ts +0 -4
- package/src/entries.ts +4 -1
- package/src/index.ts +1 -0
- package/src/layouts/PreviewLayout.astro +7 -2
- package/src/pages/[locale]/[slug].astro +1 -1
- 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 +2 -2
- package/src/tests/no_en_dash.test.ts +70 -0
- package/src/tests/pagespeed_best_practices.test.ts +198 -0
- package/src/tests/script_density.test.ts +94 -0
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/luggage-calculator/component.astro +1 -0
- package/src/tool/luggage-calculator/i18n/de.ts +1 -1
- package/src/tool/luggage-calculator/i18n/fr.ts +6 -6
- package/src/tool/luggage-calculator/i18n/pl.ts +3 -3
- package/src/tool/luggage-calculator/i18n/ru.ts +6 -6
- package/src/tool/luggage-calculator/i18n/sv.ts +1 -1
- package/src/tool/luggage-calculator/i18n/zh.ts +5 -5
- package/src/tool/mini-adventures/i18n/de.ts +2 -2
- package/src/tool/mini-adventures/i18n/en.ts +2 -2
- package/src/tool/mini-adventures/i18n/es.ts +1 -1
- package/src/tool/mini-adventures/i18n/fr.ts +2 -2
- package/src/tool/mini-adventures/i18n/id.ts +1 -1
- package/src/tool/mini-adventures/i18n/it.ts +1 -1
- package/src/tool/mini-adventures/i18n/nl.ts +1 -1
- package/src/tool/mini-adventures/i18n/pl.ts +1 -1
- package/src/tool/mini-adventures/i18n/pt.ts +1 -1
- package/src/tool/mini-adventures/i18n/ru.ts +9 -9
- package/src/tool/mini-adventures/i18n/sv.ts +2 -2
- package/src/tool/mini-adventures/i18n/zh.ts +4 -4
- package/src/tool/schengen-calculator/bibliography.astro +6 -0
- package/src/tool/schengen-calculator/bibliography.ts +18 -0
- package/src/tool/schengen-calculator/component.astro +199 -0
- package/src/tool/schengen-calculator/controller.ts +232 -0
- package/src/tool/schengen-calculator/dom-views.ts +122 -0
- package/src/tool/schengen-calculator/entry.ts +31 -0
- package/src/tool/schengen-calculator/evaluator.ts +74 -0
- package/src/tool/schengen-calculator/i18n/de.ts +274 -0
- package/src/tool/schengen-calculator/i18n/en.ts +274 -0
- package/src/tool/schengen-calculator/i18n/es.ts +274 -0
- package/src/tool/schengen-calculator/i18n/fr.ts +274 -0
- package/src/tool/schengen-calculator/i18n/id.ts +274 -0
- package/src/tool/schengen-calculator/i18n/it.ts +274 -0
- package/src/tool/schengen-calculator/i18n/ja.ts +273 -0
- package/src/tool/schengen-calculator/i18n/ko.ts +273 -0
- package/src/tool/schengen-calculator/i18n/nl.ts +274 -0
- package/src/tool/schengen-calculator/i18n/pl.ts +274 -0
- package/src/tool/schengen-calculator/i18n/pt.ts +274 -0
- package/src/tool/schengen-calculator/i18n/ru.ts +274 -0
- package/src/tool/schengen-calculator/i18n/sv.ts +274 -0
- package/src/tool/schengen-calculator/i18n/tr.ts +274 -0
- package/src/tool/schengen-calculator/i18n/zh.ts +271 -0
- package/src/tool/schengen-calculator/index.ts +10 -0
- package/src/tool/schengen-calculator/logic.test.ts +79 -0
- package/src/tool/schengen-calculator/logic.ts +164 -0
- package/src/tool/schengen-calculator/schengen-calculator.css +565 -0
- package/src/tool/schengen-calculator/seo.astro +14 -0
- package/src/tool/schengen-calculator/storage.ts +38 -0
- package/src/tool/schengen-calculator/ui.ts +38 -0
- package/src/tool/suitcase-checklist/component.astro +1 -1
- package/src/tool/suitcase-checklist/i18n/de.ts +1 -1
- package/src/tool/suitcase-checklist/i18n/fr.ts +3 -3
- package/src/tool/suitcase-checklist/i18n/ru.ts +2 -2
- package/src/tool/suitcase-checklist/i18n/zh.ts +2 -2
- package/src/tool/tip-calculator/component.astro +2 -1
- package/src/tool/tip-calculator/i18n/de.ts +1 -1
- package/src/tool/tip-calculator/i18n/fr.ts +2 -2
- package/src/tool/tip-calculator/i18n/ru.ts +5 -5
- package/src/tool/tip-calculator/i18n/sv.ts +6 -6
- package/src/tool/tip-calculator/i18n/zh.ts +4 -4
- package/src/tools.ts +2 -0
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jjlmoya/utils-travel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.ts",
|
|
9
9
|
"./data": "./src/data.ts",
|
|
10
|
-
"./entries": "./src/entries.ts"
|
|
10
|
+
"./entries": "./src/entries.ts",
|
|
11
|
+
"./runtime/*": "./src/tool/*/index.ts",
|
|
12
|
+
"./category-seo": "./src/category/seo.astro"
|
|
11
13
|
},
|
|
12
14
|
"files": [
|
|
13
15
|
"src",
|
|
@@ -26,7 +28,7 @@
|
|
|
26
28
|
"check": "astro check",
|
|
27
29
|
"type-check": "astro check",
|
|
28
30
|
"test": "vitest run",
|
|
29
|
-
"preversion": "npm run lint && npm run test",
|
|
31
|
+
"preversion": "npm run lint && npm run test && npm run build",
|
|
30
32
|
"postversion": "git push && git push --tags",
|
|
31
33
|
"patch": "npm version patch",
|
|
32
34
|
"minor": "npm version minor",
|
package/src/category/i18n/de.ts
CHANGED
|
@@ -16,7 +16,7 @@ export const content: CategoryLocaleContent = {
|
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
type: 'paragraph',
|
|
19
|
-
html: 'Von der Einhaltung der Fluglinien-Vorschriften bis hin zur internationalen kulturellen Etikette und der Gamifizierung Ihrer unmittelbaren Umgebung
|
|
19
|
+
html: 'Von der Einhaltung der Fluglinien-Vorschriften bis hin zur internationalen kulturellen Etikette und der Gamifizierung Ihrer unmittelbaren Umgebung - unsere Dienstprogramme helfen Ihnen, ein bewussterer, effizienterer und neugierigerer Reisender zu sein.',
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
type: 'title',
|
|
@@ -124,7 +124,7 @@ export const content: CategoryLocaleContent = {
|
|
|
124
124
|
},
|
|
125
125
|
{
|
|
126
126
|
type: 'paragraph',
|
|
127
|
-
html: 'Der Wechsel von Zeitzonen beeinflusst Ihren zirkadianen Rhythmus tiefgreifend. Unser <strong>Jetlag-Planer</strong>, der auf dem Prinzip der "hellen Lichttherapie" basiert, schlägt vor, wann Sie sich je nach Zielort dem Sonnenlicht aussetzen sollten. Reisen nach Osten ist schwieriger als Reisen nach Westen. Einfache Strategien wie die Anpassung Ihres Schlafes 2
|
|
127
|
+
html: 'Der Wechsel von Zeitzonen beeinflusst Ihren zirkadianen Rhythmus tiefgreifend. Unser <strong>Jetlag-Planer</strong>, der auf dem Prinzip der "hellen Lichttherapie" basiert, schlägt vor, wann Sie sich je nach Zielort dem Sonnenlicht aussetzen sollten. Reisen nach Osten ist schwieriger als Reisen nach Westen. Einfache Strategien wie die Anpassung Ihres Schlafes 2-3 Tage vor der Reise können den Jetlag drastisch reduzieren.',
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
type: 'title',
|
|
@@ -169,7 +169,7 @@ export const content: CategoryLocaleContent = {
|
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
type: 'paragraph',
|
|
172
|
-
html: 'Im Jahr 2026 sind <strong>Slow Travel</strong> und regenerativer Tourismus grundlegend. Es geht darum, weniger, aber besser zu reisen. Diese Tools befähigen Sie, Ihre Autonomie als Reisender zu verwalten, den logistischen Lärm zu reduzieren, damit Sie sich auf das konzentrieren können, was wirklich zählt: das Erlebnis und die Verbindung zum Ort. Eine gut geplante Reise ist nicht langweilig
|
|
172
|
+
html: 'Im Jahr 2026 sind <strong>Slow Travel</strong> und regenerativer Tourismus grundlegend. Es geht darum, weniger, aber besser zu reisen. Diese Tools befähigen Sie, Ihre Autonomie als Reisender zu verwalten, den logistischen Lärm zu reduzieren, damit Sie sich auf das konzentrieren können, was wirklich zählt: das Erlebnis und die Verbindung zum Ort. Eine gut geplante Reise ist nicht langweilig - sie ist befreiend.',
|
|
173
173
|
},
|
|
174
174
|
{
|
|
175
175
|
type: 'stats',
|
package/src/category/i18n/en.ts
CHANGED
|
@@ -169,7 +169,7 @@ export const content: CategoryLocaleContent = {
|
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
type: 'paragraph',
|
|
172
|
-
html: 'In 2026, <strong>Slow Travel</strong> and regenerative tourism are fundamental. It\'s about traveling less, but better. These tools empower you to manage your autonomy as a traveler, reducing logistical noise so you can focus on what really matters: the experience and the connection with the place. A well-planned trip is not boring
|
|
172
|
+
html: 'In 2026, <strong>Slow Travel</strong> and regenerative tourism are fundamental. It\'s about traveling less, but better. These tools empower you to manage your autonomy as a traveler, reducing logistical noise so you can focus on what really matters: the experience and the connection with the place. A well-planned trip is not boring-it\'s liberating.',
|
|
173
173
|
},
|
|
174
174
|
{
|
|
175
175
|
type: 'stats',
|
package/src/category/i18n/es.ts
CHANGED
|
@@ -169,7 +169,7 @@ export const content: CategoryLocaleContent = {
|
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
type: 'paragraph',
|
|
172
|
-
html: 'En 2026, el <strong>Slow Travel</strong> y el turismo regenerativo son fundamentales. Se trata de viajar menos, pero mejor. Estas herramientas te empoderan para gestionar tu autonomía como viajero, reduciendo el ruido logístico para que puedas centrarte en lo que realmente importa: la experiencia y la conexión con el lugar. Un viaje bien planificado no es aburrido
|
|
172
|
+
html: 'En 2026, el <strong>Slow Travel</strong> y el turismo regenerativo son fundamentales. Se trata de viajar menos, pero mejor. Estas herramientas te empoderan para gestionar tu autonomía como viajero, reduciendo el ruido logístico para que puedas centrarte en lo que realmente importa: la experiencia y la conexión con el lugar. Un viaje bien planificado no es aburrido-es liberador.',
|
|
173
173
|
},
|
|
174
174
|
{
|
|
175
175
|
type: 'stats',
|
package/src/category/i18n/fr.ts
CHANGED
|
@@ -7,7 +7,7 @@ export const content: CategoryLocaleContent = {
|
|
|
7
7
|
seo: [
|
|
8
8
|
{
|
|
9
9
|
type: 'title',
|
|
10
|
-
text: 'Logistique de Voyage et Exploration Locale
|
|
10
|
+
text: 'Logistique de Voyage et Exploration Locale: Planifiez intelligemment',
|
|
11
11
|
level: 2,
|
|
12
12
|
},
|
|
13
13
|
{
|
|
@@ -20,7 +20,7 @@ export const content: CategoryLocaleContent = {
|
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
type: 'title',
|
|
23
|
-
text: 'Logistique Aérienne
|
|
23
|
+
text: 'Logistique Aérienne: Calculateur de Bagages et Mesures',
|
|
24
24
|
level: 2,
|
|
25
25
|
},
|
|
26
26
|
{
|
|
@@ -29,7 +29,7 @@ export const content: CategoryLocaleContent = {
|
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
type: 'title',
|
|
32
|
-
text: 'Étiquette Internationale
|
|
32
|
+
text: 'Étiquette Internationale: Guide et Calculateur de Pourboires',
|
|
33
33
|
level: 2,
|
|
34
34
|
},
|
|
35
35
|
{
|
|
@@ -38,7 +38,7 @@ export const content: CategoryLocaleContent = {
|
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
type: 'title',
|
|
41
|
-
text: 'Organisation Efficace
|
|
41
|
+
text: 'Organisation Efficace: Générateur de Checklist de Valise',
|
|
42
42
|
level: 2,
|
|
43
43
|
},
|
|
44
44
|
{
|
|
@@ -47,7 +47,7 @@ export const content: CategoryLocaleContent = {
|
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
type: 'title',
|
|
50
|
-
text: 'Exploration de l\'Environnement
|
|
50
|
+
text: 'Exploration de l\'Environnement: Générateur de Mini Aventures',
|
|
51
51
|
level: 2,
|
|
52
52
|
},
|
|
53
53
|
{
|
|
@@ -70,12 +70,12 @@ export const content: CategoryLocaleContent = {
|
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
type: 'title',
|
|
73
|
-
text: 'Planification Financière
|
|
73
|
+
text: 'Planification Financière: Budget et Contrôle des Dépenses',
|
|
74
74
|
level: 2,
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
type: 'paragraph',
|
|
78
|
-
html: 'Un voyage sans budget est un voyage qui se contrôle lui-même plutôt que l\'inverse. Avant de partir, établissez un budget réaliste réparti par catégories
|
|
78
|
+
html: 'Un voyage sans budget est un voyage qui se contrôle lui-même plutôt que l\'inverse. Avant de partir, établissez un budget réaliste réparti par catégories: hébergement (40%), alimentation (25%), activités (20%), transport local (10%), urgences (5%). Cette structure vous évite les mauvaises surprises et maximise la valeur de chaque euro investi.',
|
|
79
79
|
},
|
|
80
80
|
{
|
|
81
81
|
type: 'paragraph',
|
|
@@ -92,12 +92,12 @@ export const content: CategoryLocaleContent = {
|
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
94
|
type: 'title',
|
|
95
|
-
text: 'Expériences Authentiques
|
|
95
|
+
text: 'Expériences Authentiques: Tourisme Responsable versus Exploitation',
|
|
96
96
|
level: 2,
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
99
|
type: 'paragraph',
|
|
100
|
-
html: 'Tous les tours ne se valent pas. Un tour "exotique" promouvant des "rencontres avec des tribus locales" est souvent de l\'exploitation touristique. La différence est
|
|
100
|
+
html: 'Tous les tours ne se valent pas. Un tour "exotique" promouvant des "rencontres avec des tribus locales" est souvent de l\'exploitation touristique. La différence est: Les locaux consentent-ils volontairement ? Reçoivent-ils une compensation équitable ? Leur vie privée et leur culture sont-elles respectées ? Voyager de manière responsable signifie effectuer des recherches préalables, respecter les limites locales et apporter un soutien économique direct aux communautés, pas aux intermédiaires.',
|
|
101
101
|
},
|
|
102
102
|
{
|
|
103
103
|
type: 'title',
|
|
@@ -115,7 +115,7 @@ export const content: CategoryLocaleContent = {
|
|
|
115
115
|
},
|
|
116
116
|
{
|
|
117
117
|
type: 'paragraph',
|
|
118
|
-
html: 'Le taux de change n\'est pas constant. Vous changez de l\'argent à un taux moins avantageux à l\'aéroport qu\'à la maison. Notre <strong>suivi de taux de change</strong> vous montre les taux historiques afin que vous puissiez planifier quand changer de l\'argent de manière optimale. L\'analyse historique montre des schémas
|
|
118
|
+
html: 'Le taux de change n\'est pas constant. Vous changez de l\'argent à un taux moins avantageux à l\'aéroport qu\'à la maison. Notre <strong>suivi de taux de change</strong> vous montre les taux historiques afin que vous puissiez planifier quand changer de l\'argent de manière optimale. L\'analyse historique montre des schémas: l\'euro a tendance à s\'affaiblir en hiver, le dollar se renforce en période de crise.',
|
|
119
119
|
},
|
|
120
120
|
{
|
|
121
121
|
type: 'title',
|
|
@@ -128,7 +128,7 @@ export const content: CategoryLocaleContent = {
|
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
type: 'title',
|
|
131
|
-
text: 'Itinéraires Optimisés
|
|
131
|
+
text: 'Itinéraires Optimisés: L\'Algorithme du Voyageur de Commerce Appliqué au Tourisme',
|
|
132
132
|
level: 2,
|
|
133
133
|
},
|
|
134
134
|
{
|
|
@@ -137,16 +137,16 @@ export const content: CategoryLocaleContent = {
|
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
type: 'title',
|
|
140
|
-
text: 'Réglementations de Voyage
|
|
140
|
+
text: 'Réglementations de Voyage: Visas, Vaccins et Documents Requis',
|
|
141
141
|
level: 2,
|
|
142
142
|
},
|
|
143
143
|
{
|
|
144
144
|
type: 'paragraph',
|
|
145
|
-
html: 'Chaque pays a ses propres exigences uniques. L\'<strong>auditeur d\'exigences de voyage</strong> vérifie votre nationalité, votre destination et vos dates de voyage pour vous informer exactement des documents dont vous avez besoin
|
|
145
|
+
html: 'Chaque pays a ses propres exigences uniques. L\'<strong>auditeur d\'exigences de voyage</strong> vérifie votre nationalité, votre destination et vos dates de voyage pour vous informer exactement des documents dont vous avez besoin: visa préalable ? vaccins obligatoires ? certificats de santé ? limites de devises ? L\'ignorer peut signifier être expulsé à la frontière ou mis à l\'amende.',
|
|
146
146
|
},
|
|
147
147
|
{
|
|
148
148
|
type: 'title',
|
|
149
|
-
text: 'Transport Local
|
|
149
|
+
text: 'Transport Local: Plans de Métro et Systèmes de Transport Globaux',
|
|
150
150
|
level: 2,
|
|
151
151
|
},
|
|
152
152
|
{
|
|
@@ -169,7 +169,7 @@ export const content: CategoryLocaleContent = {
|
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
type: 'paragraph',
|
|
172
|
-
html: 'En 2026, le <strong>Slow Travel</strong> et le tourisme régénératif sont fondamentaux. Il s\'agit de voyager moins, mais mieux. Ces outils vous permettent de gérer votre autonomie en tant que voyageur, réduisant le bruit logistique pour que vous puissiez vous concentrer sur ce qui compte vraiment
|
|
172
|
+
html: 'En 2026, le <strong>Slow Travel</strong> et le tourisme régénératif sont fondamentaux. Il s\'agit de voyager moins, mais mieux. Ces outils vous permettent de gérer votre autonomie en tant que voyageur, réduisant le bruit logistique pour que vous puissiez vous concentrer sur ce qui compte vraiment: l\'expérience et la connexion avec le lieu. Un voyage bien planifié n\'est pas ennuyeux - il est libérateur.',
|
|
173
173
|
},
|
|
174
174
|
{
|
|
175
175
|
type: 'stats',
|
package/src/category/i18n/pl.ts
CHANGED
|
@@ -16,7 +16,7 @@ export const content: CategoryLocaleContent = {
|
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
type: 'paragraph',
|
|
19
|
-
html: 'Od przestrzegania przepisów linii lotniczych po międzynarodową etykietę kulturową i grywalizację lokalnego otoczenia
|
|
19
|
+
html: 'Od przestrzegania przepisów linii lotniczych po międzynarodową etykietę kulturową i grywalizację lokalnego otoczenia - nasze narzędzia pomogą Ci być bardziej świadomym, wydajnym i ciekawym świata podróżnikiem.',
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
type: 'title',
|
package/src/category/i18n/ru.ts
CHANGED
|
@@ -12,11 +12,11 @@ export const content: CategoryLocaleContent = {
|
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
type: 'paragraph',
|
|
15
|
-
html: 'Путешествия в 2026 году
|
|
15
|
+
html: 'Путешествия в 2026 году - это сочетание открытий и технического управления. В этом разделе мы предлагаем набор <strong>бесплатных онлайн-инструментов</strong>, разработанных для цифровых кочевников, часто путешествующих людей и городских искателей приключений, которые хотят снизить стресс от планирования. Логистика поездки должна быть не препятствием для приключений, а картой, которая делает их возможными.',
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
type: 'paragraph',
|
|
19
|
-
html: 'От соблюдения правил авиакомпаний до международного культурного этикета и геймификации вашего локального окружения
|
|
19
|
+
html: 'От соблюдения правил авиакомпаний до международного культурного этикета и геймификации вашего локального окружения - наши утилиты помогут вам стать более осознанным, эффективным и любознательным путешественником.',
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
type: 'title',
|
|
@@ -34,7 +34,7 @@ export const content: CategoryLocaleContent = {
|
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
type: 'paragraph',
|
|
37
|
-
html: 'Чаевые
|
|
37
|
+
html: 'Чаевые - это не просто транзакция; это культурная норма, которая сильно различается в разных странах. Наш <strong>международный калькулятор чаевых</strong> предлагает рекомендуемые проценты и этикет чаевых для более чем 50 стран, помогая вам быть путешественником, уважающим местные обычаи, от Японии до Соединенных Штатов.',
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
type: 'title',
|
|
@@ -75,7 +75,7 @@ export const content: CategoryLocaleContent = {
|
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
type: 'paragraph',
|
|
78
|
-
html: 'Поездка без бюджета
|
|
78
|
+
html: 'Поездка без бюджета - это поездка, которая управляет сама собой, а не наоборот. Перед отъездом установите реалистичный бюджет с разбивкой по категориям: проживание (40%), питание (25%), мероприятия (20%), местный транспорт (10%), непредвиденные расходы (5%). Эта структура предотвращает неприятные сюрпризы.',
|
|
79
79
|
},
|
|
80
80
|
{
|
|
81
81
|
type: 'title',
|
package/src/category/i18n/sv.ts
CHANGED
|
@@ -16,7 +16,7 @@ export const content: CategoryLocaleContent = {
|
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
type: 'paragraph',
|
|
19
|
-
html: 'Från att följa flygbolagens regler till internationell kulturell etikett och gamification av din närmiljö
|
|
19
|
+
html: 'Från att följa flygbolagens regler till internationell kulturell etikett och gamification av din närmiljö - våra verktyg hjälper dig att bli en mer medveten, effektiv och nyfiken resenär.',
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
type: 'title',
|
package/src/data.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
export { travelCategory } from './category';
|
|
2
|
-
export { LUGGAGE_CALCULATOR_TOOL } from './tool/luggage-calculator';
|
|
3
|
-
export { TIP_CALCULATOR_TOOL } from './tool/tip-calculator';
|
|
4
|
-
export { SUITCASE_CHECKLIST_TOOL } from './tool/suitcase-checklist';
|
|
5
|
-
export { MINI_ADVENTURES_TOOL } from './tool/mini-adventures';
|
|
6
2
|
|
|
7
3
|
export type {
|
|
8
4
|
KnownLocale,
|
package/src/entries.ts
CHANGED
|
@@ -6,9 +6,12 @@ export { suitcaseChecklist } from './tool/suitcase-checklist/entry';
|
|
|
6
6
|
export type { ChecklistItem, ChecklistCategory, SuitcaseChecklistUI } from './tool/suitcase-checklist/entry';
|
|
7
7
|
export { tipCalculator } from './tool/tip-calculator/entry';
|
|
8
8
|
export type { TipCountry, TipCalculatorUI } from './tool/tip-calculator/entry';
|
|
9
|
+
export { schengenCalculator } from './tool/schengen-calculator/entry';
|
|
10
|
+
export type { SchengenCalculatorUI } from './tool/schengen-calculator/entry';
|
|
9
11
|
export { travelCategory } from './category';
|
|
10
12
|
import { luggageCalculator } from './tool/luggage-calculator/entry';
|
|
11
13
|
import { miniAdventures } from './tool/mini-adventures/entry';
|
|
12
14
|
import { suitcaseChecklist } from './tool/suitcase-checklist/entry';
|
|
13
15
|
import { tipCalculator } from './tool/tip-calculator/entry';
|
|
14
|
-
|
|
16
|
+
import { schengenCalculator } from './tool/schengen-calculator/entry';
|
|
17
|
+
export const ALL_ENTRIES = [luggageCalculator, miniAdventures, suitcaseChecklist, tipCalculator, schengenCalculator];
|
package/src/index.ts
CHANGED
|
@@ -20,3 +20,4 @@ export { luggageCalculator, LUGGAGE_CALCULATOR_TOOL } from './tool/luggage-calcu
|
|
|
20
20
|
export { tipCalculator, TIP_CALCULATOR_TOOL } from './tool/tip-calculator';
|
|
21
21
|
export { suitcaseChecklist, SUITCASE_CHECKLIST_TOOL } from './tool/suitcase-checklist';
|
|
22
22
|
export { miniAdventures, MINI_ADVENTURES_TOOL } from './tool/mini-adventures';
|
|
23
|
+
export { schengenCalculator, SCHENGEN_CALCULATOR_TOOL } from './tool/schengen-calculator';
|
|
@@ -10,7 +10,12 @@ interface Props {
|
|
|
10
10
|
hasSidebar?: boolean;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const {
|
|
13
|
+
const {
|
|
14
|
+
title,
|
|
15
|
+
currentLocale = "es",
|
|
16
|
+
localeUrls = {},
|
|
17
|
+
hasSidebar = false,
|
|
18
|
+
} = Astro.props;
|
|
14
19
|
---
|
|
15
20
|
|
|
16
21
|
<!doctype html>
|
|
@@ -78,6 +83,7 @@ const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Ast
|
|
|
78
83
|
transition:
|
|
79
84
|
background-color 0.3s ease,
|
|
80
85
|
color 0.3s ease;
|
|
86
|
+
font-family: Inter, sans-serif;
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
main {
|
|
@@ -114,4 +120,3 @@ const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Ast
|
|
|
114
120
|
}
|
|
115
121
|
}
|
|
116
122
|
</style>
|
|
117
|
-
|
|
@@ -15,7 +15,7 @@ export async function getStaticPaths() {
|
|
|
15
15
|
const paths = [];
|
|
16
16
|
|
|
17
17
|
for (const { entry, Component: lazyComp } of ALL_TOOLS) {
|
|
18
|
-
const { default: Component } = await lazyComp();
|
|
18
|
+
const { default: Component } = (await lazyComp()) as { default: unknown };
|
|
19
19
|
const localeEntries = Object.entries(entry.i18n) as [
|
|
20
20
|
KnownLocale,
|
|
21
21
|
() => Promise<ToolLocaleContent>,
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
|
|
4
|
+
type LocaleWithDiacritics = keyof typeof DIACRITIC_RULES;
|
|
5
|
+
|
|
6
|
+
const DIACRITIC_RULES = {
|
|
7
|
+
de: {
|
|
8
|
+
language: 'German',
|
|
9
|
+
expectedCharacters: 'ä ö ü ß',
|
|
10
|
+
characters: /[äöüÄÖÜß]/g,
|
|
11
|
+
minPerThousandLetters: 0.1,
|
|
12
|
+
},
|
|
13
|
+
es: {
|
|
14
|
+
language: 'Spanish',
|
|
15
|
+
expectedCharacters: 'á é í ó ú ü ñ',
|
|
16
|
+
characters: /[áéíóúüñÁÉÍÓÚÜÑ]/g,
|
|
17
|
+
minPerThousandLetters: 0.1,
|
|
18
|
+
},
|
|
19
|
+
fr: {
|
|
20
|
+
language: 'French',
|
|
21
|
+
expectedCharacters: 'à â æ ç é è ê ë î ï ô œ ù û ü ÿ',
|
|
22
|
+
characters: /[àâæçéèêëîïôœùûüÿÀÂÆÇÉÈÊËÎÏÔŒÙÛÜŸ]/g,
|
|
23
|
+
minPerThousandLetters: 0.1,
|
|
24
|
+
},
|
|
25
|
+
it: {
|
|
26
|
+
language: 'Italian',
|
|
27
|
+
expectedCharacters: 'à è é ì í î ò ó ù ú',
|
|
28
|
+
characters: /[àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ]/g,
|
|
29
|
+
minPerThousandLetters: 0.1,
|
|
30
|
+
},
|
|
31
|
+
pl: {
|
|
32
|
+
language: 'Polish',
|
|
33
|
+
expectedCharacters: 'ą ć ę ł ń ó ś ź ż',
|
|
34
|
+
characters: /[ąćęłńóśźżĄĆĘŁŃÓŚŹŻ]/g,
|
|
35
|
+
minPerThousandLetters: 0.1,
|
|
36
|
+
},
|
|
37
|
+
pt: {
|
|
38
|
+
language: 'Portuguese',
|
|
39
|
+
expectedCharacters: 'á â ã à ç é ê í ó ô õ ú ü',
|
|
40
|
+
characters: /[áâãàçéêíóôõúüÁÂÃÀÇÉÊÍÓÔÕÚÜ]/g,
|
|
41
|
+
minPerThousandLetters: 0.1,
|
|
42
|
+
},
|
|
43
|
+
sv: {
|
|
44
|
+
language: 'Swedish',
|
|
45
|
+
expectedCharacters: 'å ä ö',
|
|
46
|
+
characters: /[åäöÅÄÖ]/g,
|
|
47
|
+
minPerThousandLetters: 0.1,
|
|
48
|
+
},
|
|
49
|
+
tr: {
|
|
50
|
+
language: 'Turkish',
|
|
51
|
+
expectedCharacters: 'ç ğ ı İ ö ş ü',
|
|
52
|
+
characters: /[çğıöşüÇĞİÖŞÜ]/g,
|
|
53
|
+
minPerThousandLetters: 0.1,
|
|
54
|
+
},
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
57
|
+
const LETTERS = /\p{L}/gu;
|
|
58
|
+
const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
|
|
59
|
+
|
|
60
|
+
function collectStrings(value: unknown): string[] {
|
|
61
|
+
if (typeof value === 'string') return [value];
|
|
62
|
+
if (!value || typeof value !== 'object') return [];
|
|
63
|
+
if (Array.isArray(value)) return value.flatMap(collectStrings);
|
|
64
|
+
return Object.values(value).flatMap(collectStrings);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeText(value: unknown): string {
|
|
68
|
+
return collectStrings(value).join(' ').normalize('NFC');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function translatableContent(content: Record<string, unknown>) {
|
|
72
|
+
return TRANSLATABLE_KEYS.map((key) => content[key]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function letterCount(text: string): number {
|
|
76
|
+
return text.match(LETTERS)?.length ?? 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function diacriticCount(text: string, locale: LocaleWithDiacritics): number {
|
|
80
|
+
return text.match(DIACRITIC_RULES[locale].characters)?.length ?? 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function diacriticsPerThousandLetters(text: string, locale: LocaleWithDiacritics): number {
|
|
84
|
+
const letters = letterCount(text);
|
|
85
|
+
if (letters === 0) return 0;
|
|
86
|
+
return diacriticCount(text, locale) / letters * 1000;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('Diacritics density validation', () => {
|
|
90
|
+
ALL_TOOLS.forEach((tool) => {
|
|
91
|
+
describe(`Tool: ${tool.entry.id}`, () => {
|
|
92
|
+
Object.keys(DIACRITIC_RULES).forEach((locale) => {
|
|
93
|
+
it(`${locale} keeps the expected accent and special-letter set`, async () => {
|
|
94
|
+
const typedLocale = locale as LocaleWithDiacritics;
|
|
95
|
+
const loader = tool.entry.i18n[typedLocale];
|
|
96
|
+
if (!loader) return;
|
|
97
|
+
|
|
98
|
+
const content = await loader();
|
|
99
|
+
const text = normalizeText(translatableContent(content as Record<string, unknown>));
|
|
100
|
+
const rule = DIACRITIC_RULES[typedLocale];
|
|
101
|
+
const letters = letterCount(text);
|
|
102
|
+
const matches = diacriticCount(text, typedLocale);
|
|
103
|
+
const density = diacriticsPerThousandLetters(text, typedLocale);
|
|
104
|
+
|
|
105
|
+
expect(
|
|
106
|
+
density,
|
|
107
|
+
[
|
|
108
|
+
`Possible spelling or encoding issue detected in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
|
|
109
|
+
`The text has ${matches} special characters (${density.toFixed(2)} per 1000 letters, ${letters} letters analyzed).`,
|
|
110
|
+
`This locale should include some of these characters: ${rule.expectedCharacters}.`,
|
|
111
|
+
'If the count is 0 or near 0, accents, tildes, or special letters were probably stripped by encoding or normalization.',
|
|
112
|
+
].join(' '),
|
|
113
|
+
).toBeGreaterThanOrEqual(rule.minPerThousandLetters);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
|
|
4
|
+
const INVERTED_PUNCTUATION_LOCALES = {
|
|
5
|
+
es: {
|
|
6
|
+
language: 'Spanish',
|
|
7
|
+
questionStart: '¿',
|
|
8
|
+
questionEnd: '?',
|
|
9
|
+
exclamationStart: '¡',
|
|
10
|
+
exclamationEnd: '!',
|
|
11
|
+
},
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
type InvertedPunctuationLocale = keyof typeof INVERTED_PUNCTUATION_LOCALES;
|
|
15
|
+
|
|
16
|
+
const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
|
|
17
|
+
|
|
18
|
+
function collectStrings(value: unknown): string[] {
|
|
19
|
+
if (typeof value === 'string') return [value];
|
|
20
|
+
if (!value || typeof value !== 'object') return [];
|
|
21
|
+
if (Array.isArray(value)) return value.flatMap(collectStrings);
|
|
22
|
+
return Object.values(value).flatMap(collectStrings);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function translatableStrings(content: Record<string, unknown>): string[] {
|
|
26
|
+
return TRANSLATABLE_KEYS.flatMap((key) => collectStrings(content[key]));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sentenceStart(text: string, endIndex: number): string {
|
|
30
|
+
const beforeMark = text.slice(0, endIndex).trimEnd();
|
|
31
|
+
const boundary = Math.max(
|
|
32
|
+
beforeMark.lastIndexOf('.'),
|
|
33
|
+
beforeMark.lastIndexOf(':'),
|
|
34
|
+
beforeMark.lastIndexOf(';'),
|
|
35
|
+
beforeMark.lastIndexOf('\n'),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return beforeMark.slice(boundary + 1).trimStart();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findMissingInvertedMarks(
|
|
42
|
+
text: string,
|
|
43
|
+
startMark: string,
|
|
44
|
+
endMark: string,
|
|
45
|
+
): string[] {
|
|
46
|
+
return [...text.matchAll(new RegExp(`\\${endMark}`, 'g'))]
|
|
47
|
+
.map((match) => sentenceStart(text, match.index ?? 0))
|
|
48
|
+
.filter((segment) => segment.length > 0 && !segment.includes(startMark))
|
|
49
|
+
.map((segment) => `${segment}${endMark}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('Inverted punctuation validation', () => {
|
|
53
|
+
ALL_TOOLS.forEach((tool) => {
|
|
54
|
+
describe(`Tool: ${tool.entry.id}`, () => {
|
|
55
|
+
Object.keys(INVERTED_PUNCTUATION_LOCALES).forEach((locale) => {
|
|
56
|
+
it(`${locale} uses opening punctuation marks for questions and exclamations`, async () => {
|
|
57
|
+
const typedLocale = locale as InvertedPunctuationLocale;
|
|
58
|
+
const loader = tool.entry.i18n[typedLocale];
|
|
59
|
+
if (!loader) return;
|
|
60
|
+
|
|
61
|
+
const rule = INVERTED_PUNCTUATION_LOCALES[typedLocale];
|
|
62
|
+
const content = await loader();
|
|
63
|
+
const strings = translatableStrings(content as Record<string, unknown>);
|
|
64
|
+
const missingQuestions = strings.flatMap((text) =>
|
|
65
|
+
findMissingInvertedMarks(text, rule.questionStart, rule.questionEnd)
|
|
66
|
+
);
|
|
67
|
+
const missingExclamations = strings.flatMap((text) =>
|
|
68
|
+
findMissingInvertedMarks(text, rule.exclamationStart, rule.exclamationEnd)
|
|
69
|
+
);
|
|
70
|
+
const failures = [...missingQuestions, ...missingExclamations];
|
|
71
|
+
|
|
72
|
+
expect(
|
|
73
|
+
failures,
|
|
74
|
+
[
|
|
75
|
+
`Missing opening punctuation marks in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
|
|
76
|
+
`Questions must use ${rule.questionStart}...${rule.questionEnd} and exclamations must use ${rule.exclamationStart}...${rule.exclamationEnd}.`,
|
|
77
|
+
`Examples: ${failures.slice(0, 5).join(' | ')}`,
|
|
78
|
+
].join(' '),
|
|
79
|
+
).toHaveLength(0);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -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
|
+
});
|