feat(tours): check a city walk's stops off for the day it is on (CW-03)

The walk's detail opened from a day lists its stops in order, each with its
stay, importance and protection, and marks one visited or skipped for that
day, or takes it back. The day row shows how many were visited. Marks reach
the other travellers at once and change no route, place or order; a copy or
another day starts with none.
This commit is contained in:
Maurice
2026-10-19 22:38:03 +02:00
parent 4c594876ef
commit 290961cced
39 changed files with 618 additions and 3 deletions
+17
View File
@@ -0,0 +1,17 @@
import type { TourVisitListResponse, TourVisitUpdateRequest } from '@trek/shared';
import apiClient from './client';
/** CW-03: the stations of a trip's city walks visited or skipped on their days. */
export const tourVisitsApi = {
list: (tripId: number | string): Promise<TourVisitListResponse> =>
apiClient.get(`/trips/${tripId}/tour-visits`).then((r) => r.data),
update: (
tripId: number | string,
assignmentId: number,
stationKey: string,
body: TourVisitUpdateRequest
): Promise<TourVisitListResponse> =>
apiClient
.put(`/trips/${tripId}/tour-visits/${assignmentId}/${encodeURIComponent(stationKey)}`, body)
.then((r) => r.data),
};
+2
View File
@@ -19,6 +19,8 @@ export const HANDLED_OUTSIDE_TRIP_STORE = [
// Tours facet invalidation — hooks/useTripWebSocket forwards tours:changed to
// useTourPlaceIds so the active trip refreshes its Tours/Places read model.
'tours:changed',
// CW-03 — components/Tours/walk/useTourVisitsSync keeps the visits store current.
'tours:visits',
// Collab — Collab/MCollab components + useTripWebSocket's collabFileSync
'collab:note:created',
'collab:note:updated',
@@ -7,6 +7,7 @@ import type { Category, Place } from '../../types';
import { formatTime } from '../../utils/formatters';
import { fs } from '../shared/DialogShell';
import { TourDayRowPills } from '../Tours/TourDayRowFacts';
import { WalkProgressPill } from '../Tours/walk/WalkStations';
import { movingTimeLabel } from '../Tours/tourFacts';
import { formatPlannedTourDuration, type TourPlannedTimes } from '../Tours/tourPresentation';
import { TimePill } from './planParts';
@@ -19,6 +20,8 @@ interface DayPlanPlaceSublineProps {
tourTimes: DayRowTourTimes | undefined;
isTourPlace: boolean;
timeFormat: string;
/** CW-03: the row's day assignment, which a city walk's progress belongs to. */
assignmentId?: number;
}
function hasPlannedTimes(tourTimes: DayRowTourTimes | undefined): boolean {
@@ -90,7 +93,7 @@ function SublineDescription({ text, isTourPlace }: { text: string; isTourPlace:
// Second line of a place row in the day plan: time, tour facts or planned
// tour durations, then the clamped description.
export function DayPlanPlaceSubline({ place, cat, tourTimes, isTourPlace, timeFormat }: DayPlanPlaceSublineProps) {
export function DayPlanPlaceSubline({ place, cat, tourTimes, isTourPlace, timeFormat, assignmentId }: DayPlanPlaceSublineProps) {
const { locale } = useTranslation();
const dayRowTour = tourTimes?.tour;
const description = place.description || place.address || cat?.name;
@@ -106,6 +109,7 @@ export function DayPlanPlaceSubline({ place, cat, tourTimes, isTourPlace, timeFo
</TimePill>
)}
{dayRowTour ? <TourDayRowPills tour={dayRowTour} /> : tourTimes && <PlannedTourPills tourTimes={tourTimes} />}
{dayRowTour && <WalkProgressPill tour={dayRowTour} assignmentId={assignmentId} />}
{description && <SublineDescription text={description} isTourPlace={isTourPlace} />}
</div>
);
@@ -2033,7 +2033,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
</Tooltip>
)}
</div>
<DayPlanPlaceSubline place={place} cat={cat} tourTimes={tourTimes} isTourPlace={!!isTourPlace} timeFormat={timeFormat} />
<DayPlanPlaceSubline place={place} cat={cat} tourTimes={tourTimes} isTourPlace={!!isTourPlace} timeFormat={timeFormat} assignmentId={assignment.id} />
{assignment.notes && (
// Day-specific note on this stop (#2163) — one muted
// caption line so the timeline shows the note exists
@@ -64,6 +64,7 @@ import {
} from './tourPresentation';
import { useTourDayAssignment } from './useTourDayAssignment';
import { useTourPermissions, type TourPermissionProps } from './useTourPermissions';
import { WalkStationsCard } from './walk/WalkStations';
interface TourDetailDialogProps extends TourPermissionProps {
tour: TourListItem;
@@ -652,6 +653,9 @@ export default function TourDetailDialog({
placeId={place.id}
/>
)}
{tour.tour_type === 'walk' && (
<WalkStationsCard tour={tour} tripId={place.trip_id} assignmentId={assignment?.id ?? null} canMark={canAssign} />
)}
{tour.tour_type === 'kayak' && <PaddleCheckSection tour={tour} />}
{tour.tour_type === 'kayak' && <PaddleWindSection placeId={place.id} start={placeStart(place)} />}
@@ -0,0 +1,180 @@
import type { TourListItem, TourVisitStatus } from '@trek/shared';
import { Check, CheckCheck, Redo2, SkipForward, Undo2 } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useTranslation } from '../../../i18n';
import { tourRepo } from '../../../repo/tourRepo';
import { useTourVisitStore } from '../../../store/tourVisitStore';
import { SoftPill } from '../../Planner/planParts';
import { DialogSection, fs } from '../../shared/DialogShell';
import { useToast } from '../../shared/Toast';
import { StationPills } from '../planner/stationParts';
import { plannerWaypointsOf, type TourPlannerWaypoint } from '../planner/tourPlannerWaypoints';
import { TourDayRowPill } from '../TourDayRowFacts';
import { CardAction } from '../tourParts';
let nextId = 0;
const freshId = () => `station-${++nextId}`;
/** CW-03: a saved walk's stations in order, read once for the dialog; null while they load or could not be read. */
function useWalkStations(tripId: number | string, placeId: number): TourPlannerWaypoint[] | null {
const [stations, setStations] = useState<{ placeId: number; points: TourPlannerWaypoint[] } | null>(null);
useEffect(() => {
const controller = new AbortController();
tourRepo
.detail(tripId, placeId, controller.signal)
.then((detail) => {
if (controller.signal.aborted) return;
const points = plannerWaypointsOf(detail.waypoints, freshId).filter((point) => !point.shaping);
setStations({ placeId, points });
})
.catch(() => {
if (!controller.signal.aborted) setStations({ placeId, points: [] });
});
return () => controller.abort();
}, [placeId, tripId]);
return stations?.placeId === placeId ? stations.points : null;
}
/** The day's status of each station, by its key. */
function useDayVisits(assignmentId: number | null) {
return useTourVisitStore((state) => (assignmentId === null ? undefined : state.byAssignment[assignmentId]));
}
/**
* CW-03: a walk's progress on one of its days, as a pill of the day row:
* how many of its stations were visited. Nothing is shown before any is.
*/
export function WalkProgressPill({ tour, assignmentId }: { tour: TourListItem; assignmentId: number | undefined }) {
const { t } = useTranslation();
const visits = useDayVisits(assignmentId ?? null);
const total = tour.station_count ?? 0;
if (tour.tour_type !== 'walk' || total === 0 || !visits) return null;
const visited = Object.values(visits).filter((status) => status === 'visited').length;
if (visited === 0 && Object.keys(visits).length === 0) return null;
const label = t('tours.walk.progress', { visited, total });
return <TourDayRowPill icon={CheckCheck} value={`${visited}/${total}`} label={label} testId="plan-walk-progress" />;
}
function StatusActions({
status,
onMark,
}: {
status: TourVisitStatus | undefined;
onMark: (status: TourVisitStatus | 'planned') => void;
}) {
const { t } = useTranslation();
if (status)
return (
<CardAction icon={<Undo2 size={12} strokeWidth={2} />} onClick={() => onMark('planned')}>
{t('tours.walk.visit.undo')}
</CardAction>
);
return (
<span className="flex flex-wrap gap-1.5">
<CardAction icon={<Check size={12} strokeWidth={2.2} />} onClick={() => onMark('visited')}>
{t('tours.walk.visit.visited')}
</CardAction>
<CardAction icon={<SkipForward size={12} strokeWidth={2} />} onClick={() => onMark('skipped')}>
{t('tours.walk.visit.skip')}
</CardAction>
</span>
);
}
/**
* CW-03: a city walk's stations in its detail, in order, each with its stay,
* importance and protection; opened from a day, each can be marked visited or
* skipped for that day and taken back. Marking changes no route, place or order.
*/
export function WalkStationsCard({
tour,
tripId,
assignmentId,
canMark,
}: {
tour: TourListItem;
tripId: number | string;
/** The day the dialog was opened from; without one there is nothing to check off. */
assignmentId: number | null;
canMark: boolean;
}) {
const { t } = useTranslation();
const toast = useToast();
const stations = useWalkStations(tripId, tour.place_id);
const visits = useDayVisits(assignmentId);
const mark = useTourVisitStore((state) => state.mark);
if (tour.tour_type !== 'walk') return null;
const onMark = (key: string, status: TourVisitStatus | 'planned') => {
if (assignmentId === null) return;
mark(tripId, assignmentId, key, status).catch(() => toast.error(t('tours.walk.visit.failed')));
};
const visited = stations
? stations.filter((station) => station.key && visits?.[station.key] === 'visited').length
: 0;
return (
<DialogSection
className="flex-none"
label={t('tours.walk.stations')}
action={
stations && assignmentId !== null ? (
<SoftPill>{t('tours.walk.progress', { visited, total: stations.length })}</SoftPill>
) : undefined
}
>
<div data-testid="walk-stations" className="flex flex-col gap-2">
{stations === null && (
<span className="text-content-muted" style={fs(11.5, 'body')}>
{t('common.loading')}
</span>
)}
{assignmentId === null && stations && stations.length > 0 && (
<span className="text-content-faint" style={fs(11, 'body')}>
{t('tours.walk.visit.fromDay')}
</span>
)}
<ol className="m-0 flex list-none flex-col gap-1.5 p-0">
{(stations ?? []).map((station, index) => {
const status = station.key ? visits?.[station.key] : undefined;
return (
<li
key={station.id}
data-testid="walk-station"
className={`flex items-start gap-2.5 rounded-[10px] px-1.5 py-1.5 ${status ? 'opacity-70' : ''}`}
>
<span
className="grid h-6 w-6 flex-none place-items-center rounded-full bg-surface-tertiary font-geist tabular-nums text-content-secondary"
style={fs(11)}
>
{status === 'visited' ? (
<Check size={12} strokeWidth={2.4} />
) : status === 'skipped' ? (
<Redo2 size={12} strokeWidth={2} />
) : (
index + 1
)}
</span>
<span className="flex min-w-0 flex-1 flex-col gap-1">
<span
className={`truncate font-semibold text-content ${status === 'skipped' ? 'line-through' : ''}`}
style={fs(12.5, 'body')}
>
{station.name ?? t('tours.walk.stationNumbered', { number: index + 1 })}
</span>
<StationPills point={station} />
{status && (
<span className="text-content-faint" style={fs(11, 'body')}>
{t(status === 'visited' ? 'tours.walk.visit.isVisited' : 'tours.walk.visit.isSkipped')}
</span>
)}
</span>
{canMark && assignmentId !== null && station.key && (
<StatusActions status={status} onMark={(next) => onMark(station.key ?? '', next)} />
)}
</li>
);
})}
</ol>
</div>
</DialogSection>
);
}
@@ -0,0 +1,39 @@
import { tourVisitSchema } from '@trek/shared';
import { useEffect } from 'react';
import { addListener, removeListener } from '../../../api/websocket';
import { useTourVisitStore } from '../../../store/tourVisitStore';
import type { WebSocketEvent } from '../../../types';
/**
* CW-03: the visits of a trip's city walks, read when the trip opens and kept
* current by its socket, so a station another traveller checks off shows on
* every day plan at once. Offline the day plan shows none until it is back.
*/
export function useTourVisitsSync(tripId: number | string | undefined, enabled: boolean) {
useEffect(() => {
if (!enabled || tripId === undefined) return;
let current = true;
useTourVisitStore
.getState()
.load(tripId)
.catch(() => {
// Offline or refused, the day plan shows no progress rather than another trip's.
if (current) useTourVisitStore.getState().reset(tripId);
});
const onEvent = (event: WebSocketEvent) => {
if (event?.type !== 'tours:visits' || String(event.tripId ?? '') !== String(tripId)) return;
const visits = Array.isArray(event.visits)
? event.visits.flatMap((raw) => {
const parsed = tourVisitSchema.safeParse(raw);
return parsed.success ? [parsed.data] : [];
})
: [];
useTourVisitStore.getState().applyRemote(Number(event.assignmentId), visits);
};
addListener(onEvent);
return () => {
current = false;
removeListener(onEvent);
};
}, [enabled, tripId]);
}
@@ -1,3 +1,4 @@
import { useTourVisitsSync } from '../../components/Tours/walk/useTourVisitsSync'
import { useRoadtripSettings } from '../../hooks/useRoadtripSettings'
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import { useParams, useNavigate, useSearchParams } from 'react-router'
@@ -284,6 +285,8 @@ export function useTripPlanner() {
const [toursMode, setToursMode] = useState(false)
const previousToursModeRef = useRef(false)
const { tours, toursLoading, tourDataReady, tourPlaceIds, reloadTourPlaceIds, invalidateTourPlaceIds, upsertTour } = useTourPlaceIds(tripId, toursEnabled)
// CW-03: how far the trip's city walks got on their days, for the day plan and the walks' stations.
useTourVisitsSync(tripId, toursEnabled)
useEffect(() => {
if (!toursEnabled) {
+24
View File
@@ -0,0 +1,24 @@
import type { TourVisitListResponse, TourVisitUpdateRequest } from '@trek/shared';
import { tourVisitsApi } from '../api/tourVisits';
import { requireOnline } from './onlineOnly';
/**
* CW-03, online only for now: a station checked off is the server's at once,
* so everyone on the trip sees the same day. Offline the station says so and
* waits; the client keeps no copy of its own.
*/
export const tourVisitRepo = {
async list(tripId: number | string): Promise<TourVisitListResponse> {
requireOnline();
return await tourVisitsApi.list(tripId);
},
async update(
tripId: number | string,
assignmentId: number,
stationKey: string,
body: TourVisitUpdateRequest
): Promise<TourVisitListResponse> {
requireOnline();
return await tourVisitsApi.update(tripId, assignmentId, stationKey, body);
},
};
+68
View File
@@ -0,0 +1,68 @@
import type { TourVisit, TourVisitStatus } from '@trek/shared';
import { create } from 'zustand';
import { tourVisitRepo } from '../repo/tourVisitRepo';
/** A station's status on one day of its walk; a station not listed is planned. */
type VisitsByStation = Readonly<Record<string, TourVisitStatus>>;
interface TourVisitState {
/** The trip the visits were read for. */
tripId: string | null;
/** Per day assignment, the stations visited or skipped. */
byAssignment: Readonly<Record<number, VisitsByStation>>;
load: (tripId: number | string) => Promise<void>;
/** Nothing known for this trip: its visits could not be read. */
reset: (tripId: number | string) => void;
/** Marks a station, at once on screen; a write the server refuses puts it back and throws. */
mark: (
tripId: number | string,
assignmentId: number,
stationKey: string,
status: TourVisitStatus | 'planned'
) => Promise<void>;
/** Another traveller marked a station: the day's visits as the server has them now. */
applyRemote: (assignmentId: number, visits: readonly TourVisit[]) => void;
}
function byStation(visits: readonly TourVisit[]): VisitsByStation {
return Object.fromEntries(visits.map((visit) => [visit.station_key, visit.status]));
}
function withStatus(day: VisitsByStation | undefined, key: string, status: TourVisitStatus | 'planned') {
const next = { ...day };
if (status === 'planned') delete next[key];
else next[key] = status;
return next;
}
/**
* CW-03: how far each city walk got on its days, for the day plan's rows and
* the walk's stations. Read once a trip opens, kept current by the trip's
* socket, and written one station at a time.
*/
export const useTourVisitStore = create<TourVisitState>((set, get) => ({
tripId: null,
byAssignment: {},
load: async (tripId) => {
const { visits } = await tourVisitRepo.list(tripId);
const grouped: Record<number, Record<string, TourVisitStatus>> = {};
for (const visit of visits) (grouped[visit.assignment_id] ??= {})[visit.station_key] = visit.status;
set({ tripId: String(tripId), byAssignment: grouped });
},
reset: (tripId) => set({ tripId: String(tripId), byAssignment: {} }),
mark: async (tripId, assignmentId, stationKey, status) => {
const before = get().byAssignment[assignmentId];
set((state) => ({
byAssignment: { ...state.byAssignment, [assignmentId]: withStatus(before, stationKey, status) },
}));
try {
const { visits } = await tourVisitRepo.update(tripId, assignmentId, stationKey, { status });
get().applyRemote(assignmentId, visits);
} catch (failure) {
set((state) => ({ byAssignment: { ...state.byAssignment, [assignmentId]: before ?? {} } }));
throw failure;
}
},
applyRemote: (assignmentId, visits) =>
set((state) => ({ byAssignment: { ...state.byAssignment, [assignmentId]: byStation(visits) } })),
}));
+1
View File
@@ -62,5 +62,6 @@ export const UNRESET_STORES: Record<string, string> = {
useTourPoiStore: 'the places found along a tour; the tests that read it clear it in their own beforeEach',
useTourRouteHighlightStore:
'the stretches a bike card points at; the tests that read it clear it in their own beforeEach',
useTourVisitStore: 'the visits of city walks per day; the tests that read it reset it in their own beforeEach',
useTripJournalLinkStore: 'the journal link per trip; the tests that read it reset it in their own beforeEach',
};
+10
View File
@@ -499,6 +499,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'وقت المسار سيرًا بين المحطات، دون مدد البقاء.',
'tours.walk.stays': 'مدد البقاء',
'tours.walk.totalAtLeast': '{time} على الأقل',
'tours.walk.stations': 'محطات الجولة',
'tours.walk.stationNumbered': 'المحطة {number}',
'tours.walk.progress': 'تمت زيارة {visited} من {total}',
'tours.walk.visit.visited': 'تمت الزيارة',
'tours.walk.visit.skip': 'تخطي',
'tours.walk.visit.undo': 'تراجع',
'tours.walk.visit.isVisited': 'تمت زيارتها في هذا اليوم',
'tours.walk.visit.isSkipped': 'تم تخطيها في هذا اليوم',
'tours.walk.visit.fromDay': 'افتح الجولة من يوم في الخطة لتأشير محطاتها لذلك اليوم.',
'tours.walk.visit.failed': 'تعذر تأشير المحطة. يتطلب ذلك اتصالًا؛ حاول مجددًا بعد قليل.',
'tours.planner.tourType': 'نوع الجولة',
'tours.paddle.boat': 'القارب',
'tours.paddle.boat.kayak': 'كاياك',
+10
View File
@@ -494,6 +494,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'Dayanacaqlar arasında marşrutlaşdırıcının piyada vaxtı, qalma müddətləri olmadan.',
'tours.walk.stays': 'Qalmalar',
'tours.walk.totalAtLeast': 'ən azı {time}',
'tours.walk.stations': 'Gəzintinin dayanacaqları',
'tours.walk.stationNumbered': 'Dayanacaq {number}',
'tours.walk.progress': '{total} dayanacaqdan {visited} ziyarət edildi',
'tours.walk.visit.visited': 'Ziyarət edildi',
'tours.walk.visit.skip': 'Keç',
'tours.walk.visit.undo': 'Geri al',
'tours.walk.visit.isVisited': 'Bu gün ziyarət edildi',
'tours.walk.visit.isSkipped': 'Bu gün keçildi',
'tours.walk.visit.fromDay': 'Dayanacaqları o gün üçün qeyd etmək üçün gəzintini plandakı bir gündən aç.',
'tours.walk.visit.failed': 'Dayanacaq qeyd edilə bilmədi. Bunun üçün bağlantı lazımdır; bir azdan yenidən cəhd et.',
'tours.planner.tourType': 'Tur növü',
'tours.paddle.boat': 'Qayıq',
'tours.paddle.boat.kayak': 'Kayak', // same-as-en
+10
View File
@@ -500,6 +500,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'O tempo a pé do roteador entre as paradas, sem as permanências.',
'tours.walk.stays': 'Permanências',
'tours.walk.totalAtLeast': 'pelo menos {time}',
'tours.walk.stations': 'Paradas do passeio',
'tours.walk.stationNumbered': 'Parada {number}',
'tours.walk.progress': '{visited} de {total} visitadas',
'tours.walk.visit.visited': 'Visitada',
'tours.walk.visit.skip': 'Pular',
'tours.walk.visit.undo': 'Desfazer',
'tours.walk.visit.isVisited': 'Visitada neste dia',
'tours.walk.visit.isSkipped': 'Pulada neste dia',
'tours.walk.visit.fromDay': 'Abra o passeio a partir de um dia do plano para marcar as paradas daquele dia.',
'tours.walk.visit.failed': 'Não foi possível marcar a parada. É preciso conexão; tente de novo em instantes.',
'tours.planner.tourType': 'Tipo de passeio',
'tours.paddle.boat': 'Embarcação',
'tours.paddle.boat.kayak': 'Caiaque',
+10
View File
@@ -501,6 +501,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'El temps a peu del càlcul de ruta entre parades, sense les estades.',
'tours.walk.stays': 'Estades',
'tours.walk.totalAtLeast': 'com a mínim {time}',
'tours.walk.stations': 'Parades de la ruta',
'tours.walk.stationNumbered': 'Parada {number}',
'tours.walk.progress': '{visited} de {total} visitades',
'tours.walk.visit.visited': 'Visitada',
'tours.walk.visit.skip': 'Omet',
'tours.walk.visit.undo': 'Desfés',
'tours.walk.visit.isVisited': 'Visitada aquest dia',
'tours.walk.visit.isSkipped': 'Omesa aquest dia',
'tours.walk.visit.fromDay': "Obre la ruta des d'un dia del pla per marcar-ne les parades d'aquell dia.",
'tours.walk.visit.failed': "No s'ha pogut marcar la parada. Cal connexió; torna-ho a provar d'aquí a una estona.",
'tours.planner.tourType': 'Tipus de ruta',
'tours.paddle.boat': 'Embarcació',
'tours.paddle.boat.kayak': 'Caiac',
+10
View File
@@ -491,6 +491,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'Doba chůze mezi zastávkami podle plánovače trasy, bez pobytů.',
'tours.walk.stays': 'Pobyty',
'tours.walk.totalAtLeast': 'nejméně {time}',
'tours.walk.stations': 'Zastávky procházky',
'tours.walk.stationNumbered': 'Zastávka {number}',
'tours.walk.progress': 'Navštíveno {visited} z {total}',
'tours.walk.visit.visited': 'Navštíveno',
'tours.walk.visit.skip': 'Přeskočit',
'tours.walk.visit.undo': 'Vrátit',
'tours.walk.visit.isVisited': 'Navštíveno v tento den',
'tours.walk.visit.isSkipped': 'Přeskočeno v tento den',
'tours.walk.visit.fromDay': 'Otevři procházku ze dne v plánu, abys mohl odškrtat zastávky pro ten den.',
'tours.walk.visit.failed': 'Zastávku se nepodařilo označit. Je potřeba připojení; zkus to za chvíli znovu.',
'tours.planner.tourType': 'Typ túry',
'tours.paddle.boat': 'Loď',
'tours.paddle.boat.kayak': 'Kajak',
+11
View File
@@ -505,6 +505,17 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'Die Zeit des Routers zu Fuß zwischen den Stationen, ohne die Aufenthalte.',
'tours.walk.stays': 'Aufenthalte',
'tours.walk.totalAtLeast': 'mindestens {time}',
'tours.walk.stations': 'Stationen',
'tours.walk.stationNumbered': 'Station {number}',
'tours.walk.progress': '{visited} von {total} besucht',
'tours.walk.visit.visited': 'Besucht',
'tours.walk.visit.skip': 'Überspringen',
'tours.walk.visit.undo': 'Zurücknehmen',
'tours.walk.visit.isVisited': 'An diesem Tag besucht',
'tours.walk.visit.isSkipped': 'An diesem Tag übersprungen',
'tours.walk.visit.fromDay': 'Öffne den Walk über einen Tag im Plan, um seine Stationen für diesen Tag abzuhaken.',
'tours.walk.visit.failed':
'Die Station konnte nicht markiert werden. Dafür braucht es eine Verbindung; versuch es gleich noch einmal.',
'tours.planner.tourType': 'Tourtyp',
'tours.paddle.boat': 'Boot',
'tours.paddle.boat.kayak': 'Kajak',
+10
View File
@@ -492,6 +492,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': "The router's time on foot between the stops, without the stays.",
'tours.walk.stays': 'Stays',
'tours.walk.totalAtLeast': 'at least {time}',
'tours.walk.stations': 'Stops of the walk',
'tours.walk.stationNumbered': 'Stop {number}',
'tours.walk.progress': '{visited} of {total} visited',
'tours.walk.visit.visited': 'Mark visited',
'tours.walk.visit.skip': 'Skip',
'tours.walk.visit.undo': 'Take back',
'tours.walk.visit.isVisited': 'Visited on this day',
'tours.walk.visit.isSkipped': 'Skipped on this day',
'tours.walk.visit.fromDay': 'Open the walk from a day of the plan to check its stops off for that day.',
'tours.walk.visit.failed': 'The stop could not be marked. It needs a connection; try again in a moment.',
'tours.planner.tourType': 'Tour type',
'tours.paddle.boat': 'Boat',
'tours.paddle.boat.kayak': 'Kayak',
+10
View File
@@ -499,6 +499,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'El tiempo a pie del planificador entre paradas, sin las estancias.',
'tours.walk.stays': 'Estancias',
'tours.walk.totalAtLeast': 'al menos {time}',
'tours.walk.stations': 'Paradas del paseo',
'tours.walk.stationNumbered': 'Parada {number}',
'tours.walk.progress': '{visited} de {total} visitadas',
'tours.walk.visit.visited': 'Visitada',
'tours.walk.visit.skip': 'Omitir',
'tours.walk.visit.undo': 'Deshacer',
'tours.walk.visit.isVisited': 'Visitada este día',
'tours.walk.visit.isSkipped': 'Omitida este día',
'tours.walk.visit.fromDay': 'Abre el paseo desde un día del plan para marcar sus paradas de ese día.',
'tours.walk.visit.failed': 'No se pudo marcar la parada. Hace falta conexión; inténtalo de nuevo en un momento.',
'tours.planner.tourType': 'Tipo de ruta',
'tours.paddle.boat': 'Embarcación',
'tours.paddle.boat.kayak': 'Kayak', // same-as-en
+10
View File
@@ -494,6 +494,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'Marsruutija kõndimisaeg peatuste vahel, ilma peatumisteta.',
'tours.walk.stays': 'Peatumised',
'tours.walk.totalAtLeast': 'vähemalt {time}',
'tours.walk.stations': 'Jalutuse peatused',
'tours.walk.stationNumbered': 'Peatus {number}',
'tours.walk.progress': '{visited} / {total} külastatud',
'tours.walk.visit.visited': 'Külastatud',
'tours.walk.visit.skip': 'Jäta vahele',
'tours.walk.visit.undo': 'Võta tagasi',
'tours.walk.visit.isVisited': 'Sel päeval külastatud',
'tours.walk.visit.isSkipped': 'Sel päeval vahele jäetud',
'tours.walk.visit.fromDay': 'Ava jalutus plaani päevast, et selle päeva peatused ära märkida.',
'tours.walk.visit.failed': 'Peatust ei õnnestunud märkida. Selleks on vaja ühendust; proovi kohe uuesti.',
'tours.planner.tourType': 'Matka tüüp',
'tours.paddle.boat': 'Paat',
'tours.paddle.boat.kayak': 'Süst',
+10
View File
@@ -509,6 +509,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': "Le temps à pied du calcul d'itinéraire entre les étapes, sans les arrêts.",
'tours.walk.stays': 'Arrêts',
'tours.walk.totalAtLeast': 'au moins {time}',
'tours.walk.stations': 'Étapes de la balade',
'tours.walk.stationNumbered': 'Étape {number}',
'tours.walk.progress': '{visited} sur {total} visitées',
'tours.walk.visit.visited': 'Visitée',
'tours.walk.visit.skip': 'Passer',
'tours.walk.visit.undo': 'Annuler',
'tours.walk.visit.isVisited': 'Visitée ce jour-là',
'tours.walk.visit.isSkipped': 'Passée ce jour-là',
'tours.walk.visit.fromDay': 'Ouvre la balade depuis un jour du plan pour cocher ses étapes ce jour-là.',
'tours.walk.visit.failed': "L'étape n'a pas pu être cochée. Il faut une connexion ; réessaie dans un instant.",
'tours.planner.tourType': 'Type de sortie',
'tours.paddle.boat': 'Embarcation',
'tours.paddle.boat.kayak': 'Kayak', // same-as-en
+10
View File
@@ -509,6 +509,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'Ο χρόνος με τα πόδια του δρομολογητή ανάμεσα στις στάσεις, χωρίς τις παραμονές.',
'tours.walk.stays': 'Παραμονές',
'tours.walk.totalAtLeast': 'τουλάχιστον {time}',
'tours.walk.stations': 'Στάσεις της βόλτας',
'tours.walk.stationNumbered': 'Στάση {number}',
'tours.walk.progress': '{visited} από {total} επισκέψεις',
'tours.walk.visit.visited': 'Επισκέφθηκα',
'tours.walk.visit.skip': 'Παράλειψη',
'tours.walk.visit.undo': 'Αναίρεση',
'tours.walk.visit.isVisited': 'Επισκέφθηκε αυτή τη μέρα',
'tours.walk.visit.isSkipped': 'Παραλείφθηκε αυτή τη μέρα',
'tours.walk.visit.fromDay': 'Άνοιξε τη βόλτα από μια μέρα του πλάνου για να τσεκάρεις τις στάσεις εκείνης της μέρας.',
'tours.walk.visit.failed': 'Η στάση δεν σημειώθηκε. Χρειάζεται σύνδεση· δοκίμασε ξανά σε λίγο.',
'tours.planner.tourType': 'Τύπος διαδρομής',
'tours.paddle.boat': 'Σκάφος',
'tours.paddle.boat.kayak': 'Καγιάκ',
+10
View File
@@ -501,6 +501,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'Az útvonaltervező gyalogos ideje a megállók között, a tartózkodások nélkül.',
'tours.walk.stays': 'Tartózkodások',
'tours.walk.totalAtLeast': 'legalább {time}',
'tours.walk.stations': 'A séta megállói',
'tours.walk.stationNumbered': '{number}. megálló',
'tours.walk.progress': '{total} megállóból {visited} meglátogatva',
'tours.walk.visit.visited': 'Meglátogatva',
'tours.walk.visit.skip': 'Kihagyás',
'tours.walk.visit.undo': 'Visszavonás',
'tours.walk.visit.isVisited': 'Ezen a napon meglátogatva',
'tours.walk.visit.isSkipped': 'Ezen a napon kihagyva',
'tours.walk.visit.fromDay': 'Nyisd meg a sétát a terv egyik napjáról, hogy arra a napra kipipáld a megállókat.',
'tours.walk.visit.failed': 'A megállót nem sikerült megjelölni. Ehhez kapcsolat kell; próbáld újra kicsit később.',
'tours.planner.tourType': 'Túratípus',
'tours.paddle.boat': 'Hajó',
'tours.paddle.boat.kayak': 'Kajak',
+10
View File
@@ -493,6 +493,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'Waktu jalan kaki dari perute di antara titik singgah, tanpa lama singgah.',
'tours.walk.stays': 'Lama singgah',
'tours.walk.totalAtLeast': 'setidaknya {time}',
'tours.walk.stations': 'Titik singgah rute',
'tours.walk.stationNumbered': 'Titik {number}',
'tours.walk.progress': '{visited} dari {total} dikunjungi',
'tours.walk.visit.visited': 'Sudah dikunjungi',
'tours.walk.visit.skip': 'Lewati',
'tours.walk.visit.undo': 'Batalkan',
'tours.walk.visit.isVisited': 'Dikunjungi pada hari ini',
'tours.walk.visit.isSkipped': 'Dilewati pada hari ini',
'tours.walk.visit.fromDay': 'Buka rute dari salah satu hari di rencana untuk mencentang titiknya pada hari itu.',
'tours.walk.visit.failed': 'Titik tidak dapat ditandai. Perlu koneksi; coba lagi sebentar lagi.',
'tours.planner.tourType': 'Jenis tur',
'tours.paddle.boat': 'Perahu',
'tours.paddle.boat.kayak': 'Kayak', // same-as-en
+10
View File
@@ -500,6 +500,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'Il tempo a piedi del calcolo percorso tra le tappe, senza le soste.',
'tours.walk.stays': 'Soste',
'tours.walk.totalAtLeast': 'almeno {time}',
'tours.walk.stations': 'Tappe del percorso',
'tours.walk.stationNumbered': 'Tappa {number}',
'tours.walk.progress': '{visited} su {total} visitate',
'tours.walk.visit.visited': 'Visitata',
'tours.walk.visit.skip': 'Salta',
'tours.walk.visit.undo': 'Annulla',
'tours.walk.visit.isVisited': 'Visitata in questo giorno',
'tours.walk.visit.isSkipped': 'Saltata in questo giorno',
'tours.walk.visit.fromDay': 'Apri il percorso da un giorno del piano per spuntare le tappe di quel giorno.',
'tours.walk.visit.failed': 'Non è stato possibile segnare la tappa. Serve una connessione; riprova tra poco.',
'tours.planner.tourType': 'Tipo di tour',
'tours.paddle.boat': 'Imbarcazione',
'tours.paddle.boat.kayak': 'Kayak', // same-as-en
+11
View File
@@ -485,6 +485,17 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'ルート計算による立ち寄り先間の徒歩時間。滞在時間は含みません。',
'tours.walk.stays': '滞在',
'tours.walk.totalAtLeast': '{time} 以上',
'tours.walk.stations': '立ち寄り先',
'tours.walk.stationNumbered': '立ち寄り先 {number}',
'tours.walk.progress': '{total} 件中 {visited} 件訪問済み',
'tours.walk.visit.visited': '訪問済み',
'tours.walk.visit.skip': 'スキップ',
'tours.walk.visit.undo': '取り消す',
'tours.walk.visit.isVisited': 'この日に訪問済み',
'tours.walk.visit.isSkipped': 'この日はスキップ',
'tours.walk.visit.fromDay': '計画の日から開くと、その日の立ち寄り先をチェックできます。',
'tours.walk.visit.failed':
'立ち寄り先をチェックできませんでした。接続が必要です。少ししてからもう一度お試しください。',
'tours.planner.tourType': 'ツアーの種類',
'tours.paddle.boat': 'ボート',
'tours.paddle.boat.kayak': 'カヤック',
+10
View File
@@ -481,6 +481,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': '경로 계산기가 낸 정류지 사이 도보 시간이며, 머무는 시간은 빠져 있습니다.',
'tours.walk.stays': '머무는 시간',
'tours.walk.totalAtLeast': '최소 {time}',
'tours.walk.stations': '산책 정류지',
'tours.walk.stationNumbered': '정류지 {number}',
'tours.walk.progress': '{total}곳 중 {visited}곳 방문',
'tours.walk.visit.visited': '방문함',
'tours.walk.visit.skip': '건너뛰기',
'tours.walk.visit.undo': '되돌리기',
'tours.walk.visit.isVisited': '이날 방문함',
'tours.walk.visit.isSkipped': '이날 건너뜀',
'tours.walk.visit.fromDay': '계획의 날짜에서 열면 그날의 정류지를 체크할 수 있습니다.',
'tours.walk.visit.failed': '정류지를 표시하지 못했습니다. 연결이 필요하니 잠시 후 다시 시도하세요.',
'tours.planner.tourType': '투어 유형',
'tours.paddle.boat': '보트',
'tours.paddle.boat.kayak': '카약',
+11
View File
@@ -503,6 +503,17 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'De looptijd van de routeplanner tussen de haltes, zonder het verblijf.',
'tours.walk.stays': 'Verblijven',
'tours.walk.totalAtLeast': 'minstens {time}',
'tours.walk.stations': 'Haltes van de wandeling',
'tours.walk.stationNumbered': 'Halte {number}',
'tours.walk.progress': '{visited} van {total} bezocht',
'tours.walk.visit.visited': 'Bezocht',
'tours.walk.visit.skip': 'Overslaan',
'tours.walk.visit.undo': 'Terugnemen',
'tours.walk.visit.isVisited': 'Op deze dag bezocht',
'tours.walk.visit.isSkipped': 'Op deze dag overgeslagen',
'tours.walk.visit.fromDay': 'Open de wandeling vanuit een dag in het plan om de haltes voor die dag af te vinken.',
'tours.walk.visit.failed':
'De halte kon niet worden gemarkeerd. Daar is verbinding voor nodig; probeer het zo opnieuw.',
'tours.planner.tourType': 'Soort tocht',
'tours.paddle.boat': 'Boot',
'tours.paddle.boat.kayak': 'Kajak',
+10
View File
@@ -508,6 +508,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'Czas marszu między przystankami według planera trasy, bez postojów.',
'tours.walk.stays': 'Postoje',
'tours.walk.totalAtLeast': 'co najmniej {time}',
'tours.walk.stations': 'Przystanki trasy',
'tours.walk.stationNumbered': 'Przystanek {number}',
'tours.walk.progress': 'Odwiedzono {visited} z {total}',
'tours.walk.visit.visited': 'Odwiedzony',
'tours.walk.visit.skip': 'Pomiń',
'tours.walk.visit.undo': 'Cofnij',
'tours.walk.visit.isVisited': 'Odwiedzony tego dnia',
'tours.walk.visit.isSkipped': 'Pominięty tego dnia',
'tours.walk.visit.fromDay': 'Otwórz trasę z dnia w planie, aby odhaczyć przystanki na ten dzień.',
'tours.walk.visit.failed': 'Nie udało się oznaczyć przystanku. Potrzebne jest połączenie; spróbuj za chwilę.',
'tours.planner.tourType': 'Rodzaj wycieczki',
'tours.paddle.boat': 'Łódź',
'tours.paddle.boat.kayak': 'Kajak',
+10
View File
@@ -511,6 +511,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'Время пешком между остановками по расчёту маршрута, без остановок на месте.',
'tours.walk.stays': 'Остановки',
'tours.walk.totalAtLeast': 'не меньше {time}',
'tours.walk.stations': 'Остановки прогулки',
'tours.walk.stationNumbered': 'Остановка {number}',
'tours.walk.progress': 'Посещено {visited} из {total}',
'tours.walk.visit.visited': 'Посещено',
'tours.walk.visit.skip': 'Пропустить',
'tours.walk.visit.undo': 'Отменить',
'tours.walk.visit.isVisited': 'Посещена в этот день',
'tours.walk.visit.isSkipped': 'Пропущена в этот день',
'tours.walk.visit.fromDay': 'Открой прогулку из дня в плане, чтобы отмечать остановки за этот день.',
'tours.walk.visit.failed': 'Не удалось отметить остановку. Нужна связь; попробуй чуть позже.',
'tours.planner.tourType': 'Тип тура',
'tours.paddle.boat': 'Лодка',
'tours.paddle.boat.kayak': 'Каяк',
+10
View File
@@ -493,6 +493,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'Čas chôdze medzi zastávkami podľa plánovača trasy, bez pobytov.',
'tours.walk.stays': 'Pobyty',
'tours.walk.totalAtLeast': 'najmenej {time}',
'tours.walk.stations': 'Zastávky prechádzky',
'tours.walk.stationNumbered': 'Zastávka {number}',
'tours.walk.progress': 'Navštívené {visited} z {total}',
'tours.walk.visit.visited': 'Navštívené',
'tours.walk.visit.skip': 'Preskočiť',
'tours.walk.visit.undo': 'Vrátiť',
'tours.walk.visit.isVisited': 'Navštívené v tento deň',
'tours.walk.visit.isSkipped': 'Preskočené v tento deň',
'tours.walk.visit.fromDay': 'Otvor prechádzku z dňa v pláne, aby si odškrtol zastávky na ten deň.',
'tours.walk.visit.failed': 'Zastávku sa nepodarilo označiť. Je potrebné pripojenie; skús to o chvíľu znova.',
'tours.planner.tourType': 'Typ túry',
'tours.paddle.boat': 'Loď',
'tours.paddle.boat.kayak': 'Kajak',
+10
View File
@@ -492,6 +492,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'Ruttplanerarens gångtid mellan stoppen, utan vistelserna.',
'tours.walk.stays': 'Vistelser',
'tours.walk.totalAtLeast': 'minst {time}',
'tours.walk.stations': 'Promenadens stopp',
'tours.walk.stationNumbered': 'Stopp {number}',
'tours.walk.progress': '{visited} av {total} besökta',
'tours.walk.visit.visited': 'Besökt',
'tours.walk.visit.skip': 'Hoppa över',
'tours.walk.visit.undo': 'Ångra',
'tours.walk.visit.isVisited': 'Besökt den här dagen',
'tours.walk.visit.isSkipped': 'Överhoppad den här dagen',
'tours.walk.visit.fromDay': 'Öppna promenaden från en dag i planen för att bocka av stoppen den dagen.',
'tours.walk.visit.failed': 'Stoppet kunde inte markeras. Det kräver en anslutning; försök igen om en stund.',
'tours.planner.tourType': 'Turtyp',
'tours.paddle.boat': 'Båt',
'tours.paddle.boat.kayak': 'Kajak',
+10
View File
@@ -475,6 +475,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'เวลาเดินระหว่างจุดแวะจากตัวคำนวณเส้นทาง ไม่รวมเวลาแวะพัก',
'tours.walk.stays': 'เวลาแวะพัก',
'tours.walk.totalAtLeast': 'อย่างน้อย {time}',
'tours.walk.stations': 'จุดแวะของทริป',
'tours.walk.stationNumbered': 'จุดแวะที่ {number}',
'tours.walk.progress': 'แวะแล้ว {visited} จาก {total}',
'tours.walk.visit.visited': 'แวะแล้ว',
'tours.walk.visit.skip': 'ข้าม',
'tours.walk.visit.undo': 'ยกเลิก',
'tours.walk.visit.isVisited': 'แวะแล้วในวันนี้',
'tours.walk.visit.isSkipped': 'ข้ามไปในวันนี้',
'tours.walk.visit.fromDay': 'เปิดทริปจากวันในแผนเพื่อติ๊กจุดแวะของวันนั้น',
'tours.walk.visit.failed': 'ทำเครื่องหมายจุดแวะไม่ได้ ต้องมีการเชื่อมต่อ ลองใหม่อีกครั้งในอีกสักครู่',
'tours.planner.tourType': 'ประเภททริป',
'tours.paddle.boat': 'เรือ',
'tours.paddle.boat.kayak': 'เรือคายัก',
+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 = 735;
const GENERAL_KEY_COUNT = 745;
function literalKeys(source: string): string[] {
const tree = ts.createSourceFile('locale.ts', source, ts.ScriptTarget.Latest, true);
+10
View File
@@ -495,6 +495,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'Rota hesaplayıcının duraklar arasındaki yürüme süresi, kalışlar hariç.',
'tours.walk.stays': 'Kalışlar',
'tours.walk.totalAtLeast': 'en az {time}',
'tours.walk.stations': 'Yürüyüşün durakları',
'tours.walk.stationNumbered': 'Durak {number}',
'tours.walk.progress': '{total} duraktan {visited} ziyaret edildi',
'tours.walk.visit.visited': 'Ziyaret edildi',
'tours.walk.visit.skip': 'Atla',
'tours.walk.visit.undo': 'Geri al',
'tours.walk.visit.isVisited': 'Bu gün ziyaret edildi',
'tours.walk.visit.isSkipped': 'Bu gün atlandı',
'tours.walk.visit.fromDay': 'Duraklarını o gün için işaretlemek için yürüyüşü plandaki bir günden aç.',
'tours.walk.visit.failed': 'Durak işaretlenemedi. Bunun için bağlantı gerekir; birazdan yeniden dene.',
'tours.planner.tourType': 'Tur türü',
'tours.paddle.boat': 'Tekne',
'tours.paddle.boat.kayak': 'Kano (kayak)',
+10
View File
@@ -507,6 +507,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'Час пішки між зупинками за розрахунком маршруту, без перебування.',
'tours.walk.stays': 'Перебування',
'tours.walk.totalAtLeast': 'щонайменше {time}',
'tours.walk.stations': 'Зупинки прогулянки',
'tours.walk.stationNumbered': 'Зупинка {number}',
'tours.walk.progress': 'Відвідано {visited} з {total}',
'tours.walk.visit.visited': 'Відвідано',
'tours.walk.visit.skip': 'Пропустити',
'tours.walk.visit.undo': 'Скасувати',
'tours.walk.visit.isVisited': 'Відвідано цього дня',
'tours.walk.visit.isSkipped': 'Пропущено цього дня',
'tours.walk.visit.fromDay': 'Відкрий прогулянку з дня в плані, щоб відмічати зупинки на цей день.',
'tours.walk.visit.failed': "Не вдалося позначити зупинку. Потрібне з'єднання; спробуй трохи згодом.",
'tours.planner.tourType': 'Тип туру',
'tours.paddle.boat': 'Човен',
'tours.paddle.boat.kayak': 'Каяк',
+10
View File
@@ -492,6 +492,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': 'Thời gian đi bộ giữa các điểm dừng do bộ định tuyến tính, không gồm thời gian dừng.',
'tours.walk.stays': 'Thời gian dừng',
'tours.walk.totalAtLeast': 'ít nhất {time}',
'tours.walk.stations': 'Các điểm dừng',
'tours.walk.stationNumbered': 'Điểm dừng {number}',
'tours.walk.progress': 'Đã ghé {visited}/{total}',
'tours.walk.visit.visited': 'Đã ghé',
'tours.walk.visit.skip': 'Bỏ qua',
'tours.walk.visit.undo': 'Hoàn tác',
'tours.walk.visit.isVisited': 'Đã ghé trong ngày này',
'tours.walk.visit.isSkipped': 'Đã bỏ qua trong ngày này',
'tours.walk.visit.fromDay': 'Mở lộ trình từ một ngày trong kế hoạch để đánh dấu các điểm dừng của ngày đó.',
'tours.walk.visit.failed': 'Không thể đánh dấu điểm dừng. Cần có kết nối; hãy thử lại sau giây lát.',
'tours.planner.tourType': 'Loại chuyến',
'tours.paddle.boat': 'Thuyền',
'tours.paddle.boat.kayak': 'Thuyền kayak',
+10
View File
@@ -460,6 +460,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': '路線規劃給出的站點間步行時間,不含停留時間。',
'tours.walk.stays': '停留',
'tours.walk.totalAtLeast': '至少 {time}',
'tours.walk.stations': '漫步站點',
'tours.walk.stationNumbered': '第 {number} 站',
'tours.walk.progress': '已到訪 {visited}/{total}',
'tours.walk.visit.visited': '已到訪',
'tours.walk.visit.skip': '略過',
'tours.walk.visit.undo': '復原',
'tours.walk.visit.isVisited': '當天已到訪',
'tours.walk.visit.isSkipped': '當天已略過',
'tours.walk.visit.fromDay': '從計畫中的某一天打開漫步,即可勾選當天的站點。',
'tours.walk.visit.failed': '無法標記該站點。需要網路連線,請稍後再試。',
'tours.planner.tourType': '路線類型',
'tours.paddle.boat': '船隻',
'tours.paddle.boat.kayak': '獨木舟(Kayak)',
+10
View File
@@ -461,6 +461,16 @@ const tours: TranslationStrings = {
'tours.walk.timeEstimatedInfo': '路线规划给出的站点间步行时间,不含停留时间。',
'tours.walk.stays': '停留',
'tours.walk.totalAtLeast': '至少 {time}',
'tours.walk.stations': '漫步站点',
'tours.walk.stationNumbered': '第 {number} 站',
'tours.walk.progress': '已到访 {visited}/{total}',
'tours.walk.visit.visited': '已到访',
'tours.walk.visit.skip': '跳过',
'tours.walk.visit.undo': '撤销',
'tours.walk.visit.isVisited': '当天已到访',
'tours.walk.visit.isSkipped': '当天已跳过',
'tours.walk.visit.fromDay': '从计划中的某一天打开漫步,即可勾选当天的站点。',
'tours.walk.visit.failed': '无法标记该站点。需要网络连接,请稍后再试。',
'tours.planner.tourType': '路线类型',
'tours.paddle.boat': '船只',
'tours.paddle.boat.kayak': '皮划艇',