feat(tours): a hike can be marked as a winter hike or a snowshoe hike

HIK-01. The details of the hiking planner gain an optional Winter activity
switch, off by default, so the normal hiking flow asks for nothing more.
Switched on, the hike is a winter hike or a snowshoe hike; the choice stays
dimmed and out of reach while the switch is off. The label is saved with the
tour, comes back when it is opened, survives a draft reload, a copy and a
take-over, and switching it off takes only the label away: the route, its
grade and the days stay as they were.

The label is a label only. Routing, the walking time and the 3D terrain view
work as for any hike, and the info dot and the head pill's tooltip say that
nothing was checked for snow, avalanches or whether a path can be walked in
winter. The saved label shows as a frosted snowflake on the corner of the
tour's tile in the tour list and the day plan, and as a pill beside the type
in the tour's detail.

The server keeps it in tours.winter_activity (CHECK list, NULL for every tour
saved before and for every other type), refuses it on a bike tour, a city walk
and a paddle tour, and keeps the stored label when an older client saves a
hike without the field. A snowshoe hike exports to GPX as snowshoeing.
list_tours names the field, with the same words on what it does not say.
This commit is contained in:
Maurice
2026-10-20 12:20:33 +02:00
parent 71fe69c1f1
commit cd407cf80c
61 changed files with 1266 additions and 24 deletions
+8
View File
@@ -406,6 +406,14 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'سيرًا على الأقدام، ضمن صعوبة تحددها',
'tours.planner.tourTypeHint.bike': 'بالدراجة، مسار مناسب للتجوال أو الطريق أو الحصى أو الجبلية',
'tours.planner.tourTypeHint.kayak': 'على الماء، بقارب الكاياك أو الكانو أو لوح التجديف',
'tours.winter.title': 'نشاط شتوي',
'tours.winter.infoLabel': 'ماذا تعني علامة الشتاء',
'tours.winter.info':
'مجرد علامة للنزهة. يبقى المسار وألوان الانحدار ووقت المشي كما في أي نزهة، ولا تقول شيئًا عن الثلج أو الانهيارات الثلجية أو ما إذا كان الطريق صالحًا للمشي في الشتاء.',
'tours.winter.kind': 'نوع النشاط الشتوي',
'tours.winter.activity.winter_hike': 'مشي شتوي',
'tours.winter.activity.snowshoe': 'مشي بأحذية الثلج',
'tours.winter.labelOnly': 'وضعها المسافرون. يُخطَّط المسار والوقت كما في أي نزهة، دون فحص لظروف الشتاء.',
'tours.bike.profile': 'الملف',
'tours.bike.profile.touring': 'تجوال',
'tours.bike.profile.road': 'طريق',
+9
View File
@@ -395,6 +395,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'Piyada, təyin etdiyin çətinlik daxilində',
'tours.planner.tourTypeHint.bike': 'Velosipedlə, turinq, yol, qravel və ya MTB üçün marşrut',
'tours.planner.tourTypeHint.kayak': 'Suda, kayak, kanoe və ya SUP ilə',
'tours.winter.title': 'Qış fəaliyyəti',
'tours.winter.infoLabel': 'Qış nişanı nə edir',
'tours.winter.info':
'Yalnız yürüş üçün nişan. Marşrut, meyllilik rəngləri və yeriş vaxtı istənilən yürüşdəki kimi qalır və qar, uçqun və ya cığırın qışda keçilə bilməsi barədə heç nə demir.',
'tours.winter.kind': 'Qış fəaliyyətinin növü',
'tours.winter.activity.winter_hike': 'Qış yürüşü',
'tours.winter.activity.snowshoe': 'Qar ayaqqabısı ilə yürüş',
'tours.winter.labelOnly':
'Səyahətçilər tərəfindən qeyd edilib. Marşrut və vaxt istənilən yürüşdəki kimi planlaşdırılır, qış şəraiti üçün yoxlanılmır.',
'tours.bike.profile': 'Profil',
'tours.bike.profile.touring': 'Turinq',
'tours.bike.profile.road': 'Yol',
+9
View File
@@ -401,6 +401,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'A pé, dentro de uma dificuldade que você define',
'tours.planner.tourTypeHint.bike': 'De bicicleta, com rota para cicloturismo, estrada, gravel ou MTB',
'tours.planner.tourTypeHint.kayak': 'Na água, de caiaque, canoa ou SUP',
'tours.winter.title': 'Atividade de inverno',
'tours.winter.infoLabel': 'O que a marcação de inverno faz',
'tours.winter.info':
'Apenas uma marcação da caminhada. A rota, as cores de inclinação e o tempo de caminhada continuam como em qualquer caminhada e não dizem nada sobre neve, avalanches ou se um caminho pode ser percorrido no inverno.',
'tours.winter.kind': 'Tipo de atividade de inverno',
'tours.winter.activity.winter_hike': 'Caminhada de inverno',
'tours.winter.activity.snowshoe': 'Caminhada com raquetes de neve',
'tours.winter.labelOnly':
'Marcado pelos viajantes. Rota e tempo são planejados como em qualquer caminhada, sem verificação para o inverno.',
'tours.bike.profile': 'Perfil',
'tours.bike.profile.touring': 'Cicloturismo',
'tours.bike.profile.road': 'Estrada',
+9
View File
@@ -402,6 +402,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': "A peu, dins d'una dificultat que tu tries",
'tours.planner.tourTypeHint.bike': 'En bicicleta, amb ruta per a cicloturisme, carretera, gravel o BTT',
'tours.planner.tourTypeHint.kayak': "A l'aigua, amb caiac, canoa o SUP",
'tours.winter.title': 'Activitat hivernal',
'tours.winter.infoLabel': 'Què fa la marca hivernal',
'tours.winter.info':
'Només una marca de l’excursió. La ruta, els colors de pendent i el temps de marxa són els de qualsevol excursió i no diuen res de la neu, les allaus ni de si un camí és transitable a l’hivern.',
'tours.winter.kind': 'Tipus d’activitat hivernal',
'tours.winter.activity.winter_hike': 'Excursió hivernal',
'tours.winter.activity.snowshoe': 'Excursió amb raquetes de neu',
'tours.winter.labelOnly':
'Marcat pels viatgers. La ruta i el temps es planifiquen com en qualsevol excursió, sense comprovar l’hivern.',
'tours.bike.profile': 'Perfil',
'tours.bike.profile.touring': 'Cicloturisme',
'tours.bike.profile.road': 'Carretera',
+9
View File
@@ -394,6 +394,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'Pěšky, do obtížnosti, kterou nastavíš',
'tours.planner.tourTypeHint.bike': 'Na kole, trasa pro treking, silnici, gravel nebo MTB',
'tours.planner.tourTypeHint.kayak': 'Na vodě, na kajaku, kánoi nebo SUP',
'tours.winter.title': 'Zimní aktivita',
'tours.winter.infoLabel': 'Co dělá zimní označení',
'tours.winter.info':
'Jen označení túry. Trasa, barvy sklonu a doba chůze zůstávají jako u každé túry a neříkají nic o sněhu, lavinách ani o tom, zda je cesta v zimě schůdná.',
'tours.winter.kind': 'Druh zimní aktivity',
'tours.winter.activity.winter_hike': 'Zimní turistika',
'tours.winter.activity.snowshoe': 'Túra na sněžnicích',
'tours.winter.labelOnly':
'Označeno cestovateli. Trasa a čas se plánují jako u každé túry, bez kontroly zimních podmínek.',
'tours.bike.profile': 'Profil',
'tours.bike.profile.touring': 'Treking',
'tours.bike.profile.road': 'Silnice',
+9
View File
@@ -404,6 +404,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'Zu Fuß, bis zu einer Schwierigkeit, die du festlegst',
'tours.planner.tourTypeHint.bike': 'Mit dem Rad, geroutet für Touring, Rennrad, Gravel oder MTB',
'tours.planner.tourTypeHint.kayak': 'Auf dem Wasser, mit Kajak, Kanadier oder SUP',
'tours.winter.title': 'Winteraktivität',
'tours.winter.infoLabel': 'Was die Winterkennzeichnung bewirkt',
'tours.winter.info':
'Nur eine Kennzeichnung der Wanderung. Route, Steigungsfarben und Gehzeit bleiben wie bei jeder Wanderung und sagen nichts über Schnee, Lawinen oder die Begehbarkeit im Winter.',
'tours.winter.kind': 'Art der Winteraktivität',
'tours.winter.activity.winter_hike': 'Winterwandern',
'tours.winter.activity.snowshoe': 'Schneeschuhwandern',
'tours.winter.labelOnly':
'Von den Reisenden gekennzeichnet. Route und Zeit sind wie bei jeder Wanderung geplant, nicht für den Winter geprüft.',
'tours.bike.profile': 'Profil',
'tours.bike.profile.touring': 'Touring', // same-as-en
'tours.bike.profile.road': 'Rennrad',
+9
View File
@@ -394,6 +394,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'On foot, within a difficulty you set',
'tours.planner.tourTypeHint.bike': 'By bike, routed for touring, road, gravel or MTB',
'tours.planner.tourTypeHint.kayak': 'On the water, by kayak, canoe or SUP',
'tours.winter.title': 'Winter activity',
'tours.winter.infoLabel': 'What the winter label does',
'tours.winter.info':
'A label for the hike only. Route, grade colours and walking time stay as for any hike and say nothing about snow, avalanches or whether a path can be walked in winter.',
'tours.winter.kind': 'Kind of winter activity',
'tours.winter.activity.winter_hike': 'Winter hike',
'tours.winter.activity.snowshoe': 'Snowshoe hike',
'tours.winter.labelOnly':
'Marked by the travellers. Route and time are planned as for any hike, not checked for winter.',
'tours.bike.profile': 'Profile',
'tours.bike.profile.touring': 'Touring',
'tours.bike.profile.road': 'Road',
+9
View File
@@ -401,6 +401,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'A pie, dentro de una dificultad que tú eliges',
'tours.planner.tourTypeHint.bike': 'En bici, con ruta para cicloturismo, carretera, gravel o MTB',
'tours.planner.tourTypeHint.kayak': 'En el agua, en kayak, canoa o SUP',
'tours.winter.title': 'Actividad invernal',
'tours.winter.infoLabel': 'Qué hace la marca invernal',
'tours.winter.info':
'Solo una marca de la ruta. El trazado, los colores de pendiente y el tiempo de marcha son los de cualquier ruta y no dicen nada sobre nieve, aludes ni si un camino es transitable en invierno.',
'tours.winter.kind': 'Tipo de actividad invernal',
'tours.winter.activity.winter_hike': 'Senderismo invernal',
'tours.winter.activity.snowshoe': 'Ruta con raquetas de nieve',
'tours.winter.labelOnly':
'Marcado por los viajeros. Ruta y tiempo se planifican como en cualquier ruta, sin comprobación invernal.',
'tours.bike.profile': 'Perfil',
'tours.bike.profile.touring': 'Cicloturismo',
'tours.bike.profile.road': 'Carretera',
+9
View File
@@ -396,6 +396,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'Jalgsi, sinu määratud raskuse piires',
'tours.planner.tourTypeHint.bike': 'Rattaga, marsruut matka-, maantee-, gravel- või maastikurattale',
'tours.planner.tourTypeHint.kayak': 'Vee peal, süsta, kanuu või SUP-iga',
'tours.winter.title': 'Talvine tegevus',
'tours.winter.infoLabel': 'Mida talvemärge teeb',
'tours.winter.info':
'Ainult matka märge. Marsruut, tõusuvärvid ja kõnniaeg jäävad samaks nagu igal matkal ega ütle midagi lume, laviinide ega selle kohta, kas rada on talvel läbitav.',
'tours.winter.kind': 'Talvise tegevuse liik',
'tours.winter.activity.winter_hike': 'Talvematk',
'tours.winter.activity.snowshoe': 'Räätsamatk',
'tours.winter.labelOnly':
'Reisijate märgitud. Marsruut ja aeg on kavandatud nagu igal matkal, talve jaoks kontrollimata.',
'tours.bike.profile': 'Profiil',
'tours.bike.profile.touring': 'Matk',
'tours.bike.profile.road': 'Maantee',
+9
View File
@@ -407,6 +407,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'À pied, dans la difficulté que tu choisis',
'tours.planner.tourTypeHint.bike': 'À vélo, itinéraire pour le voyage, la route, le gravel ou le VTT',
'tours.planner.tourTypeHint.kayak': "Sur l'eau, en kayak, canoë ou SUP",
'tours.winter.title': 'Activité hivernale',
'tours.winter.infoLabel': 'À quoi sert le marquage hivernal',
'tours.winter.info':
'Simple marquage de la randonnée. L’itinéraire, les couleurs de pente et le temps de marche restent ceux de toute randonnée et ne disent rien de la neige, des avalanches ni de la praticabilité d’un chemin en hiver.',
'tours.winter.kind': 'Type d’activité hivernale',
'tours.winter.activity.winter_hike': 'Randonnée hivernale',
'tours.winter.activity.snowshoe': 'Randonnée en raquettes',
'tours.winter.labelOnly':
'Marqué par les voyageurs. Itinéraire et durée sont planifiés comme pour toute randonnée, sans vérification hivernale.',
'tours.bike.profile': 'Profil',
'tours.bike.profile.touring': 'Voyage',
'tours.bike.profile.road': 'Route',
+9
View File
@@ -409,6 +409,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'Με τα πόδια, μέσα στη δυσκολία που ορίζεις',
'tours.planner.tourTypeHint.bike': 'Με ποδήλατο, διαδρομή για περιήγηση, δρόμο, gravel ή MTB',
'tours.planner.tourTypeHint.kayak': 'Στο νερό, με καγιάκ, κανό ή SUP',
'tours.winter.title': 'Χειμερινή δραστηριότητα',
'tours.winter.infoLabel': 'Τι κάνει η χειμερινή σήμανση',
'tours.winter.info':
'Μόνο μια σήμανση της πεζοπορίας. Η διαδρομή, τα χρώματα κλίσης και ο χρόνος πορείας μένουν όπως σε κάθε πεζοπορία και δεν λένε τίποτα για χιόνι, χιονοστιβάδες ή για το αν ένα μονοπάτι είναι βατό τον χειμώνα.',
'tours.winter.kind': 'Είδος χειμερινής δραστηριότητας',
'tours.winter.activity.winter_hike': 'Χειμερινή πεζοπορία',
'tours.winter.activity.snowshoe': 'Πεζοπορία με χιονοπέδιλα',
'tours.winter.labelOnly':
'Σημειώθηκε από τους ταξιδιώτες. Η διαδρομή και ο χρόνος σχεδιάζονται όπως σε κάθε πεζοπορία, χωρίς έλεγχο για τον χειμώνα.',
'tours.bike.profile': 'Προφίλ',
'tours.bike.profile.touring': 'Περιήγηση',
'tours.bike.profile.road': 'Δρόμου',
+9
View File
@@ -401,6 +401,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'Gyalog, az általad megadott nehézségig',
'tours.planner.tourTypeHint.bike': 'Kerékpárral, túra, országúti, gravel vagy MTB útvonallal',
'tours.planner.tourTypeHint.kayak': 'Vízen, kajakkal, kenuval vagy SUP-pal',
'tours.winter.title': 'Téli tevékenység',
'tours.winter.infoLabel': 'Mit jelent a téli jelölés',
'tours.winter.info':
'Csak a túra jelölése. Az útvonal, a meredekségi színek és a menetidő ugyanazok, mint bármely túránál, és semmit nem mondanak a hóról, a lavinákról vagy arról, hogy az út télen járható-e.',
'tours.winter.kind': 'A téli tevékenység fajtája',
'tours.winter.activity.winter_hike': 'Téli túra',
'tours.winter.activity.snowshoe': 'Hótalpas túra',
'tours.winter.labelOnly':
'Az utazók jelölték meg. Az útvonal és az idő úgy van tervezve, mint bármely túránál, téli szempontból nincs ellenőrizve.',
'tours.bike.profile': 'Profil',
'tours.bike.profile.touring': 'Túra',
'tours.bike.profile.road': 'Országúti',
+9
View File
@@ -396,6 +396,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'Jalan kaki, dalam tingkat kesulitan yang kamu tentukan',
'tours.planner.tourTypeHint.bike': 'Dengan sepeda, rute untuk touring, road, gravel, atau MTB',
'tours.planner.tourTypeHint.kayak': 'Di air, dengan kayak, kano, atau SUP',
'tours.winter.title': 'Aktivitas musim dingin',
'tours.winter.infoLabel': 'Apa fungsi label musim dingin',
'tours.winter.info':
'Hanya label untuk pendakian. Rute, warna kemiringan, dan waktu jalan tetap seperti pendakian lain dan tidak mengatakan apa pun tentang salju, longsoran salju, atau apakah jalur bisa dilalui di musim dingin.',
'tours.winter.kind': 'Jenis aktivitas musim dingin',
'tours.winter.activity.winter_hike': 'Pendakian musim dingin',
'tours.winter.activity.snowshoe': 'Pendakian dengan sepatu salju',
'tours.winter.labelOnly':
'Ditandai oleh para pelancong. Rute dan waktu direncanakan seperti pendakian lain, tidak diperiksa untuk musim dingin.',
'tours.bike.profile': 'Profil',
'tours.bike.profile.touring': 'Touring', // same-as-en
'tours.bike.profile.road': 'Road', // same-as-en
+9
View File
@@ -404,6 +404,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'A piedi, entro una difficoltà che scegli tu',
'tours.planner.tourTypeHint.bike': 'In bici, percorso per cicloturismo, strada, gravel o MTB',
'tours.planner.tourTypeHint.kayak': 'In acqua, in kayak, canoa o SUP',
'tours.winter.title': 'Attività invernale',
'tours.winter.infoLabel': 'Cosa fa il contrassegno invernale',
'tours.winter.info':
'Solo un contrassegno dell’escursione. Percorso, colori di pendenza e tempo di cammino restano quelli di ogni escursione e non dicono nulla su neve, valanghe o percorribilità di un sentiero in inverno.',
'tours.winter.kind': 'Tipo di attività invernale',
'tours.winter.activity.winter_hike': 'Escursione invernale',
'tours.winter.activity.snowshoe': 'Ciaspolata',
'tours.winter.labelOnly':
'Contrassegnato dai viaggiatori. Percorso e tempi sono pianificati come per ogni escursione, non verificati per l’inverno.',
'tours.bike.profile': 'Profilo',
'tours.bike.profile.touring': 'Turismo',
'tours.bike.profile.road': 'Strada',
+9
View File
@@ -386,6 +386,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': '徒歩、設定した難易度まで',
'tours.planner.tourTypeHint.bike': '自転車で、ツーリング、ロード、グラベル、MTB向けのルート',
'tours.planner.tourTypeHint.kayak': '水上で、カヤック、カヌー、SUPで',
'tours.winter.title': '冬のアクティビティ',
'tours.winter.infoLabel': '冬のラベルについて',
'tours.winter.info':
'ハイキングのラベルにすぎません。ルート、勾配の色、歩行時間は通常のハイキングと同じで、雪や雪崩、冬に道を歩けるかどうかについては何も示しません。',
'tours.winter.kind': '冬のアクティビティの種類',
'tours.winter.activity.winter_hike': '冬のハイキング',
'tours.winter.activity.snowshoe': 'スノーシューハイキング',
'tours.winter.labelOnly':
'旅行者が付けたラベルです。ルートと時間は通常のハイキングと同じく計画され、冬向けには確認されていません。',
'tours.bike.profile': 'プロファイル',
'tours.bike.profile.touring': 'ツーリング',
'tours.bike.profile.road': 'ロード',
+9
View File
@@ -384,6 +384,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': '도보로, 설정한 난이도 안에서',
'tours.planner.tourTypeHint.bike': '자전거로, 투어링, 로드, 그래블, MTB에 맞춘 경로',
'tours.planner.tourTypeHint.kayak': '물 위에서, 카약, 카누 또는 SUP로',
'tours.winter.title': '겨울 활동',
'tours.winter.infoLabel': '겨울 표시의 의미',
'tours.winter.info':
'하이킹을 위한 표시일 뿐입니다. 경로, 경사 색상, 걷는 시간은 다른 하이킹과 같으며 눈, 눈사태 또는 겨울에 길을 걸을 수 있는지에 대해서는 아무것도 알려 주지 않습니다.',
'tours.winter.kind': '겨울 활동 종류',
'tours.winter.activity.winter_hike': '겨울 하이킹',
'tours.winter.activity.snowshoe': '스노슈 하이킹',
'tours.winter.labelOnly':
'여행자가 표시했습니다. 경로와 시간은 다른 하이킹과 같이 계획되며 겨울 조건은 확인하지 않았습니다.',
'tours.bike.profile': '프로필',
'tours.bike.profile.touring': '투어링',
'tours.bike.profile.road': '로드',
+9
View File
@@ -403,6 +403,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'Te voet, binnen een moeilijkheid die je kiest',
'tours.planner.tourTypeHint.bike': 'Met de fiets, gerouteerd voor toer, race, gravel of MTB',
'tours.planner.tourTypeHint.kayak': 'Op het water, met kajak, kano of SUP',
'tours.winter.title': 'Winteractiviteit',
'tours.winter.infoLabel': 'Wat het winterlabel doet',
'tours.winter.info':
'Alleen een label voor de wandeling. Route, hellingskleuren en looptijd blijven zoals bij elke wandeling en zeggen niets over sneeuw, lawines of de begaanbaarheid van een pad in de winter.',
'tours.winter.kind': 'Soort winteractiviteit',
'tours.winter.activity.winter_hike': 'Winterwandeling',
'tours.winter.activity.snowshoe': 'Sneeuwschoenwandeling',
'tours.winter.labelOnly':
'Gemarkeerd door de reizigers. Route en tijd worden gepland als bij elke wandeling, niet gecontroleerd op de winter.',
'tours.bike.profile': 'Profiel',
'tours.bike.profile.touring': 'Toer',
'tours.bike.profile.road': 'Racefiets',
+9
View File
@@ -410,6 +410,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'Pieszo, w granicach wybranej trudności',
'tours.planner.tourTypeHint.bike': 'Na rowerze, trasa dla turystyki, szosy, gravela lub MTB',
'tours.planner.tourTypeHint.kayak': 'Na wodzie, kajakiem, kanadyjką lub SUP-em',
'tours.winter.title': 'Aktywność zimowa',
'tours.winter.infoLabel': 'Co daje oznaczenie zimowe',
'tours.winter.info':
'Tylko oznaczenie wędrówki. Trasa, kolory nachylenia i czas marszu pozostają jak przy każdej wędrówce i nic nie mówią o śniegu, lawinach ani o tym, czy ścieżka jest zimą do przejścia.',
'tours.winter.kind': 'Rodzaj aktywności zimowej',
'tours.winter.activity.winter_hike': 'Wędrówka zimowa',
'tours.winter.activity.snowshoe': 'Wędrówka na rakietach śnieżnych',
'tours.winter.labelOnly':
'Oznaczone przez podróżnych. Trasa i czas są planowane jak przy każdej wędrówce, bez sprawdzania pod kątem zimy.',
'tours.bike.profile': 'Profil',
'tours.bike.profile.touring': 'Turystyka',
'tours.bike.profile.road': 'Szosa',
+9
View File
@@ -414,6 +414,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'Пешком, в пределах выбранной сложности',
'tours.planner.tourTypeHint.bike': 'На велосипеде, маршрут для туризма, шоссе, гравела или МТБ',
'tours.planner.tourTypeHint.kayak': 'По воде, на каяке, каноэ или SUP',
'tours.winter.title': 'Зимняя активность',
'tours.winter.infoLabel': 'Что означает зимняя отметка',
'tours.winter.info':
'Только отметка похода. Маршрут, цвета крутизны и время в пути остаются как у любого похода и ничего не говорят о снеге, лавинах или проходимости тропы зимой.',
'tours.winter.kind': 'Вид зимней активности',
'tours.winter.activity.winter_hike': 'Зимний поход',
'tours.winter.activity.snowshoe': 'Поход на снегоступах',
'tours.winter.labelOnly':
'Отмечено путешественниками. Маршрут и время рассчитаны как для любого похода, без проверки на зимние условия.',
'tours.bike.profile': 'Профиль',
'tours.bike.profile.touring': 'Туризм',
'tours.bike.profile.road': 'Шоссе',
+9
View File
@@ -395,6 +395,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'Pešo, do obtiažnosti, ktorú nastavíš',
'tours.planner.tourTypeHint.bike': 'Na bicykli, trasa pre trekking, cestu, gravel alebo MTB',
'tours.planner.tourTypeHint.kayak': 'Na vode, na kajaku, kanoe alebo SUP',
'tours.winter.title': 'Zimná aktivita',
'tours.winter.infoLabel': 'Čo robí zimné označenie',
'tours.winter.info':
'Len označenie túry. Trasa, farby sklonu a čas chôdze zostávajú ako pri každej túre a nehovoria nič o snehu, lavínach ani o tom, či je cesta v zime schodná.',
'tours.winter.kind': 'Druh zimnej aktivity',
'tours.winter.activity.winter_hike': 'Zimná turistika',
'tours.winter.activity.snowshoe': 'Túra na snežniciach',
'tours.winter.labelOnly':
'Označené cestovateľmi. Trasa a čas sa plánujú ako pri každej túre, bez kontroly zimných podmienok.',
'tours.bike.profile': 'Profil',
'tours.bike.profile.touring': 'Trekking',
'tours.bike.profile.road': 'Cestný',
+9
View File
@@ -393,6 +393,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'Till fots, inom en svårighet du väljer',
'tours.planner.tourTypeHint.bike': 'Med cykel, rutt för tur, landsväg, grus eller MTB',
'tours.planner.tourTypeHint.kayak': 'På vattnet, med kajak, kanot eller SUP',
'tours.winter.title': 'Vinteraktivitet',
'tours.winter.infoLabel': 'Vad vintermärkningen gör',
'tours.winter.info':
'Bara en märkning av vandringen. Rutt, lutningsfärger och gångtid är desamma som för alla vandringar och säger inget om snö, laviner eller om en stig går att gå på vintern.',
'tours.winter.kind': 'Typ av vinteraktivitet',
'tours.winter.activity.winter_hike': 'Vintervandring',
'tours.winter.activity.snowshoe': 'Snöskovandring',
'tours.winter.labelOnly':
'Märkt av resenärerna. Rutt och tid planeras som för alla vandringar, inte kontrollerade för vinter.',
'tours.bike.profile': 'Profil',
'tours.bike.profile.touring': 'Tur',
'tours.bike.profile.road': 'Landsväg',
+9
View File
@@ -375,6 +375,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'เดินเท้า ภายในระดับความยากที่คุณกำหนด',
'tours.planner.tourTypeHint.bike': 'ด้วยจักรยาน เส้นทางสำหรับทัวริ่ง เสือหมอบ กราเวล หรือเสือภูเขา',
'tours.planner.tourTypeHint.kayak': 'บนน้ำ ด้วยคายัค แคนู หรือ SUP',
'tours.winter.title': 'กิจกรรมฤดูหนาว',
'tours.winter.infoLabel': 'ป้ายฤดูหนาวมีไว้ทำอะไร',
'tours.winter.info':
'เป็นเพียงป้ายกำกับของการเดินป่า เส้นทาง สีความชัน และเวลาเดินยังคงเหมือนการเดินป่าทั่วไป และไม่ได้บอกอะไรเกี่ยวกับหิมะ หิมะถล่ม หรือว่าเส้นทางเดินได้ในฤดูหนาวหรือไม่',
'tours.winter.kind': 'ประเภทกิจกรรมฤดูหนาว',
'tours.winter.activity.winter_hike': 'เดินป่าฤดูหนาว',
'tours.winter.activity.snowshoe': 'เดินป่าด้วยรองเท้าลุยหิมะ',
'tours.winter.labelOnly':
'ผู้เดินทางเป็นผู้ทำเครื่องหมาย เส้นทางและเวลาวางแผนเหมือนการเดินป่าทั่วไป ไม่ได้ตรวจสอบสำหรับฤดูหนาว',
'tours.bike.profile': 'โปรไฟล์',
'tours.bike.profile.touring': 'ทัวริ่ง',
'tours.bike.profile.road': 'เสือหมอบ',
+1 -1
View File
@@ -20,7 +20,7 @@ const tokens = (value: string) => [...value.matchAll(/\{[^{}]+\}/g)].map((match)
// placeholder spec; here only the general keys are.
const groups = pluralGroups(Object.keys(english));
const generalKeys = (keys: string[]) => keys.filter((key) => !pluralFormOf(key, groups));
const GENERAL_KEY_COUNT = 825;
const GENERAL_KEY_COUNT = 832;
function literalKeys(source: string): string[] {
const tree = ts.createSourceFile('locale.ts', source, ts.ScriptTarget.Latest, true);
+9
View File
@@ -395,6 +395,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'Yürüyerek, belirlediğin zorluk içinde',
'tours.planner.tourTypeHint.bike': 'Bisikletle, tur, yol, gravel veya MTB için rota',
'tours.planner.tourTypeHint.kayak': 'Suda, kano, kanoe veya SUP ile',
'tours.winter.title': 'Kış etkinliği',
'tours.winter.infoLabel': 'Kış etiketi ne işe yarar',
'tours.winter.info':
'Yalnızca yürüyüş için bir etiket. Rota, eğim renkleri ve yürüme süresi her yürüyüşteki gibi kalır; kar, çığ ya da bir yolun kışın yürünebilir olup olmadığı hakkında hiçbir şey söylemez.',
'tours.winter.kind': 'Kış etkinliği türü',
'tours.winter.activity.winter_hike': 'Kış yürüyüşü',
'tours.winter.activity.snowshoe': 'Kar ayakkabısıyla yürüyüş',
'tours.winter.labelOnly':
'Gezginler tarafından işaretlendi. Rota ve süre her yürüyüşteki gibi planlanır, kış koşullarına göre denetlenmez.',
'tours.bike.profile': 'Profil',
'tours.bike.profile.touring': 'Tur',
'tours.bike.profile.road': 'Yol',
+9
View File
@@ -409,6 +409,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'Пішки, у межах обраної складності',
'tours.planner.tourTypeHint.bike': 'На велосипеді, маршрут для туризму, шосе, гревелу або МТБ',
'tours.planner.tourTypeHint.kayak': 'По воді, на каяку, каное або SUP',
'tours.winter.title': 'Зимова активність',
'tours.winter.infoLabel': 'Що означає зимова позначка',
'tours.winter.info':
'Лише позначка походу. Маршрут, кольори крутизни й час у дорозі залишаються як для будь-якого походу й нічого не кажуть про сніг, лавини чи прохідність стежки взимку.',
'tours.winter.kind': 'Вид зимової активності',
'tours.winter.activity.winter_hike': 'Зимовий похід',
'tours.winter.activity.snowshoe': 'Похід на снігоступах',
'tours.winter.labelOnly':
'Позначено мандрівниками. Маршрут і час сплановано як для будь-якого походу, без перевірки на зимові умови.',
'tours.bike.profile': 'Профіль',
'tours.bike.profile.touring': 'Туризм',
'tours.bike.profile.road': 'Шосе',
+9
View File
@@ -393,6 +393,15 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': 'Đi bộ, trong mức độ khó bạn chọn',
'tours.planner.tourTypeHint.bike': 'Bằng xe đạp, lộ trình cho touring, đường trường, gravel hoặc MTB',
'tours.planner.tourTypeHint.kayak': 'Trên nước, bằng kayak, xuồng canoe hoặc SUP',
'tours.winter.title': 'Hoạt động mùa đông',
'tours.winter.infoLabel': 'Nhãn mùa đông có tác dụng gì',
'tours.winter.info':
'Chỉ là nhãn cho chuyến đi bộ. Tuyến đường, màu độ dốc và thời gian đi bộ vẫn như mọi chuyến đi bộ khác và không cho biết gì về tuyết, tuyết lở hay việc lối đi có đi được vào mùa đông không.',
'tours.winter.kind': 'Loại hoạt động mùa đông',
'tours.winter.activity.winter_hike': 'Đi bộ mùa đông',
'tours.winter.activity.snowshoe': 'Đi bộ bằng giày tuyết',
'tours.winter.labelOnly':
'Do du khách đánh dấu. Tuyến đường và thời gian được lập như mọi chuyến đi bộ khác, không kiểm tra cho mùa đông.',
'tours.bike.profile': 'Hồ sơ',
'tours.bike.profile.touring': 'Du lịch',
'tours.bike.profile.road': 'Đường trường',
+7
View File
@@ -360,6 +360,13 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': '步行,在你設定的難度內',
'tours.planner.tourTypeHint.bike': '騎乘,為旅行車、公路車、礫石車或登山車規劃路線',
'tours.planner.tourTypeHint.kayak': '水上,獨木舟、加拿大艇或立槳',
'tours.winter.title': '冬季活動',
'tours.winter.infoLabel': '冬季標記的作用',
'tours.winter.info': '只是健行的標記。路線、坡度顏色與步行時間與任何健行相同,不說明積雪、雪崩或道路冬季是否可通行。',
'tours.winter.kind': '冬季活動類型',
'tours.winter.activity.winter_hike': '冬季健行',
'tours.winter.activity.snowshoe': '雪鞋健行',
'tours.winter.labelOnly': '由旅行者標記。路線與時間按一般健行規劃,未針對冬季查核。',
'tours.bike.profile': '類型',
'tours.bike.profile.touring': '旅行',
'tours.bike.profile.road': '公路',
+7
View File
@@ -361,6 +361,13 @@ const tours: TranslationStrings = {
'tours.planner.tourTypeHint.hike': '步行,在你设定的难度内',
'tours.planner.tourTypeHint.bike': '骑行,为旅行车、公路车、砾石车或山地车规划路线',
'tours.planner.tourTypeHint.kayak': '水上,皮划艇、独木舟或桨板',
'tours.winter.title': '冬季活动',
'tours.winter.infoLabel': '冬季标记的作用',
'tours.winter.info': '只是徒步的标记。路线、坡度颜色和步行时间与任何徒步相同,不说明积雪、雪崩或道路冬季是否可通行。',
'tours.winter.kind': '冬季活动类型',
'tours.winter.activity.winter_hike': '冬季徒步',
'tours.winter.activity.snowshoe': '雪鞋徒步',
'tours.winter.labelOnly': '由旅行者标记。路线和时间按普通徒步规划,未针对冬季核查。',
'tours.bike.profile': '类型',
'tours.bike.profile.touring': '旅行',
'tours.bike.profile.road': '公路',
+56
View File
@@ -16,6 +16,7 @@ import {
tourMaxHikingDifficultySchema,
tourNameSchema,
tourWebsiteSchema,
WINTER_ACTIVITIES,
} from './tours.schema';
import { describe, expect, it } from 'vitest';
@@ -534,3 +535,58 @@ describe('city walks (CW-01 to CW-03, CW-10)', () => {
expect(tourDescriptionSchema.safeParse('x'.repeat(2001)).success).toBe(false);
});
});
describe('winter activity of a hike (HIK-01)', () => {
const listedHike = {
place_id: 1,
tour_type: 'hike' as const,
distance: 8,
elevation_gain: 400,
elevation_loss: 400,
duration: 180,
difficulty: null,
wanderer_ref: null,
match_confidence: 1,
max_hiking_difficulty: 2,
name: 'Snowy ridge',
planned: false,
caution: false,
};
it('offers exactly winter hiking and snowshoeing, and a hike needs neither', () => {
expect(WINTER_ACTIVITIES).toEqual(['winter_hike', 'snowshoe']);
expect(tourCreateRequestSchema.parse(baseRequest).winter_activity).toBeUndefined();
expect(tourCreateRequestSchema.parse({ ...baseRequest, winter_activity: null }).winter_activity).toBeNull();
});
it('keeps the winter label a hike is saved with', () => {
for (const activity of WINTER_ACTIVITIES) {
expect(tourCreateRequestSchema.parse({ ...baseRequest, winter_activity: activity })).toMatchObject({
tour_type: 'hike',
winter_activity: activity,
});
}
});
it('refuses a label it does not know, and any label on another type', () => {
expect(tourCreateRequestSchema.safeParse({ ...baseRequest, winter_activity: 'ski_tour' }).success).toBe(false);
const bike = { ...baseRequest, tour_type: 'bike', bike_profile: 'mtb', winter_activity: 'snowshoe' };
expect(() => tourCreateRequestSchema.parse(bike)).toThrow(/a bike tour has no winter activity/);
const walk = { ...baseRequest, tour_type: 'walk', winter_activity: 'winter_hike' };
expect(() => tourCreateRequestSchema.parse(walk)).toThrow(/a city walk has no winter activity/);
const paddle = {
...baseRequest,
tour_type: 'kayak',
boat_type: 'sup',
water_context: 'lake',
winter_activity: 'snowshoe',
};
expect(() => tourCreateRequestSchema.parse(paddle)).toThrow(/a paddle tour has no winter activity/);
});
it('lists the label, and reads a list cached before it existed', () => {
expect(tourListItemSchema.parse({ ...listedHike, winter_activity: 'snowshoe' }).winter_activity).toBe('snowshoe');
expect(tourListItemSchema.parse(listedHike).winter_activity).toBeUndefined();
expect(tourListItemSchema.safeParse({ ...listedHike, winter_activity: 'ski_tour' }).success).toBe(false);
});
});
+17
View File
@@ -58,6 +58,16 @@ export type PaddleWaterContext = z.infer<typeof paddleWaterContextSchema>;
export const paddleLakeStyleSchema = z.enum(['direct', 'shore']);
export type PaddleLakeStyle = z.infer<typeof paddleLakeStyleSchema>;
/**
* HIK-01: a hike marked as one done in winter, walked or on snowshoes. It is
* optional and only a label: the route, its grade and its time are planned as
* for any hike, and the label says nothing about whether a way can be walked
* in winter, about snow or about avalanches. A hike without one keeps null.
*/
export const WINTER_ACTIVITIES = ['winter_hike', 'snowshoe'] as const;
export const winterActivitySchema = z.enum(WINTER_ACTIVITIES);
export type WinterActivity = z.infer<typeof winterActivitySchema>;
/** The highest planning speed a paddler can enter, in km/h. */
export const MAX_PADDLE_SPEED_KMH = 20;
export const paddleSpeedKmhSchema = z.number().finite().min(0.5).max(MAX_PADDLE_SPEED_KMH);
@@ -165,6 +175,8 @@ export const tourSchema = z.object({
derived_from_place_id: z.number().int().positive().nullable().optional(),
/** BIK-01: a bike tour's profile; null for every other type. Absent in a list cached before it existed. */
bike_profile: bikeProfileSchema.nullable().optional(),
/** HIK-01: a hike's winter label; null for none and for every other type. Absent in a list cached before it existed. */
winter_activity: winterActivitySchema.nullable().optional(),
/** A paddle tour's boat and water; null for every other type. Absent in a list cached before they existed. */
boat_type: paddleBoatTypeSchema.nullable().optional(),
water_context: paddleWaterContextSchema.nullable().optional(),
@@ -401,6 +413,8 @@ export const tourCreateRequestSchema = z
hiking_preferences: hikingPreferencesSchema.nullable().optional(),
/** BIK-01, a bike tour's only, and its one requirement. */
bike_profile: bikeProfileSchema.nullable().optional(),
/** HIK-01, a hike's only: its winter label, null to take it off; left out, an edit keeps the one it has. */
winter_activity: winterActivitySchema.nullable().optional(),
boat_type: paddleBoatTypeSchema.nullable().optional(),
water_context: paddleWaterContextSchema.nullable().optional(),
paddle_speed_kmh: paddleSpeedKmhSchema.nullable().optional(),
@@ -493,6 +507,7 @@ function tourTypeIssues(value: {
max_hiking_difficulty?: number | null;
hiking_preferences?: HikingPreferences | null;
bike_profile?: BikeProfile | null;
winter_activity?: WinterActivity | null;
boat_type?: PaddleBoatType | null;
water_context?: PaddleWaterContext | null;
paddle_speed_kmh?: number | null;
@@ -526,6 +541,8 @@ function tourTypeIssues(value: {
issues.push({ path: ['max_hiking_difficulty'], message: `${named} has no hiking difficulty` });
if (hikingPreferencesOf(value.hiking_preferences) !== null)
issues.push({ path: ['hiking_preferences'], message: `${named} has no hiking preferences` });
if (value.winter_activity != null)
issues.push({ path: ['winter_activity'], message: `${named} has no winter activity` });
}
if (type === 'bike' && value.bike_profile == null)
issues.push({ path: ['bike_profile'], message: 'a bike tour needs a bike profile' });