mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
feat(tours): a city walk's arrivals against its stops' opening hours, and a compact place search (CW-04, CW-05)
Opened from a day with a time, each stop shows its estimated arrival: the start, the walking time shared out along the route, and the stays before it, at the earliest while a stay is unknown. A stop whose opening hours say it is closed then, or closes before the stay ends, says so with the times; hours that cannot be read are shown as they are, and a stop without hours is unknown, never open. The themes are a two by two grid and the categories one line with a list to tick, so the search no longer fills the rail. The button that discards a draft for a new tour says so in two words.
This commit is contained in:
@@ -654,7 +654,22 @@ export default function TourDetailDialog({
|
||||
/>
|
||||
)}
|
||||
{tour.tour_type === 'walk' && (
|
||||
<WalkStationsCard tour={tour} tripId={place.trip_id} assignmentId={assignment?.id ?? null} canMark={canAssign} />
|
||||
<WalkStationsCard
|
||||
tour={tour}
|
||||
tripId={place.trip_id}
|
||||
assignmentId={assignment?.id ?? null}
|
||||
canMark={canAssign}
|
||||
day={
|
||||
assignment
|
||||
? {
|
||||
date: days.find((day) => day.id === assignment.day_id)?.date ?? null,
|
||||
start: assignment.assignment_time ?? place.place_time ?? null,
|
||||
route: analysis?.routeCoordinates ?? [],
|
||||
walkingMinutes: tour.duration ?? null,
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{tour.tour_type === 'kayak' && <PaddleCheckSection tour={tour} />}
|
||||
{tour.tour_type === 'kayak' && <PaddleWindSection placeId={place.id} start={placeStart(place)} />}
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import type { TourPoi, TourPoiCategory, WalkTheme } from '@trek/shared';
|
||||
import type { TourPoi, WalkTheme } from '@trek/shared';
|
||||
import { TOUR_POI_CORRIDORS_M, WALK_THEMES } from '@trek/shared';
|
||||
import {
|
||||
AlertTriangle,
|
||||
ChevronDown,
|
||||
Clock,
|
||||
Coffee,
|
||||
ExternalLink,
|
||||
Landmark,
|
||||
Loader2,
|
||||
MoveHorizontal,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Route,
|
||||
Search,
|
||||
Shuffle,
|
||||
Trees,
|
||||
WifiOff,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
@@ -18,9 +24,11 @@ import { useSettingsStore } from '../../../store/settingsStore';
|
||||
import { useTourPoiStore } from '../../../store/tourPoiStore';
|
||||
import { formatDistance } from '../../../utils/units';
|
||||
import { TOUR_POI_LOOKS } from '../../Map/poiCategories';
|
||||
import { PopoverItem } from '../../Packing/PackingPopover';
|
||||
import { WhiteBadge } from '../../Planner/planParts';
|
||||
import { PillSelect, Segmented } from '../../shared/dialogParts';
|
||||
import { PillSelect, PopoverListPanel, Segmented } from '../../shared/dialogParts';
|
||||
import { fs } from '../../shared/DialogShell';
|
||||
import { usePopoverList } from '../../shared/usePopoverList';
|
||||
import type { TourPlannerController } from '../planner/useTourPlanner';
|
||||
import { useWaypointPlaceNames } from '../planner/useWaypointPlaceNames';
|
||||
import { RailCard, TourNotice } from '../tourParts';
|
||||
@@ -33,21 +41,114 @@ type Unit = Parameters<typeof formatDistance>[1];
|
||||
const km = (metres: number) => metres / 1000;
|
||||
|
||||
/** A category to search for, as a chip: its icon in its map colour, ticked or not. */
|
||||
function CategoryChip({ category, on, onToggle }: { category: TourPoiCategory; on: boolean; onToggle: () => void }) {
|
||||
/**
|
||||
* What is searched for, in one line: the chosen categories' icons and names,
|
||||
* with every category the tour type offers in a list that stays open while
|
||||
* they are ticked. A theme of a city walk sets them all at once above it.
|
||||
*/
|
||||
function CategoryPicker({ search }: { search: TourPoiSearch }) {
|
||||
const { t } = useTranslation();
|
||||
const look = TOUR_POI_LOOKS[category];
|
||||
const list = usePopoverList(0, { estimatedHeight: 320 });
|
||||
const { open, toggle, triggerRef, listId } = list;
|
||||
const chosen = search.offered.filter((category) => search.categories.includes(category));
|
||||
const names = chosen.map((category) => t(TOUR_POI_LOOKS[category].labelKey));
|
||||
const summary =
|
||||
names.length === 0
|
||||
? t('tours.poi.categoriesNone')
|
||||
: names.length <= 2
|
||||
? names.join(', ')
|
||||
: t('tours.poi.categoriesMore', { first: names.slice(0, 2).join(', '), more: names.length - 2 });
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
aria-pressed={on}
|
||||
data-testid={`tour-poi-category-${category}`}
|
||||
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 font-medium transition-colors ${on ? 'bg-surface-card text-content shadow-sm ring-1 ring-inset ring-edge-faint' : 'bg-surface-tertiary text-content-muted hover:text-content'}`}
|
||||
style={fs(11.5, 'body')}
|
||||
>
|
||||
<look.Icon size={12} strokeWidth={2.2} style={{ color: on ? look.color : undefined }} />
|
||||
{t(look.labelKey)}
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
aria-expanded={open}
|
||||
aria-controls={open ? listId : undefined}
|
||||
data-testid="tour-poi-categories"
|
||||
className="flex w-full min-w-0 items-center gap-2 rounded-[10px] border border-edge bg-surface-input px-2.5 py-1.5 text-start hover:border-content-faint"
|
||||
>
|
||||
<span className="sr-only">{t('tours.poi.categories')}: </span>
|
||||
<span className="flex flex-none -space-x-1 rtl:space-x-reverse" aria-hidden="true">
|
||||
{chosen.slice(0, 3).map((category) => {
|
||||
const look = TOUR_POI_LOOKS[category];
|
||||
return (
|
||||
<span
|
||||
key={category}
|
||||
className="grid h-5 w-5 place-items-center rounded-full bg-surface-card ring-2 ring-[color:var(--bg-card)]"
|
||||
style={{ color: look.color }}
|
||||
>
|
||||
<look.Icon size={11} strokeWidth={2.2} />
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate font-medium text-content" style={fs(12, 'body')}>
|
||||
{summary}
|
||||
</span>
|
||||
<ChevronDown
|
||||
size={14}
|
||||
strokeWidth={2.2}
|
||||
aria-hidden="true"
|
||||
className={`flex-none text-content-faint transition-transform ${open ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
<PopoverListPanel list={list} label={t('tours.poi.categories')} className="min-w-[240px]">
|
||||
{search.offered.map((category) => {
|
||||
const look = TOUR_POI_LOOKS[category];
|
||||
return (
|
||||
<PopoverItem
|
||||
key={category}
|
||||
icon={<look.Icon size={13} strokeWidth={2.2} style={{ color: look.color }} />}
|
||||
label={t(look.labelKey)}
|
||||
active={search.categories.includes(category)}
|
||||
onClick={() => search.toggleCategory(category)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</PopoverListPanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const THEME_ICONS: Record<WalkTheme, LucideIcon> = { culture: Landmark, parks: Trees, food: Coffee, mixed: Shuffle };
|
||||
|
||||
/**
|
||||
* CW-05: a city walk's four themes as a grid of two by two, each naming
|
||||
* itself in full; the one the categories below match is on.
|
||||
*/
|
||||
function ThemeGrid({
|
||||
themes,
|
||||
active,
|
||||
onChoose,
|
||||
}: {
|
||||
themes: readonly WalkTheme[];
|
||||
active: WalkTheme | null;
|
||||
onChoose: (theme: WalkTheme) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-1.5" role="group" aria-label={t('tours.walk.themes')}>
|
||||
{themes.map((theme) => {
|
||||
const Icon = THEME_ICONS[theme];
|
||||
const on = theme === active;
|
||||
return (
|
||||
<button
|
||||
key={theme}
|
||||
type="button"
|
||||
onClick={() => onChoose(theme)}
|
||||
aria-pressed={on}
|
||||
data-testid={`tour-poi-theme-${theme}`}
|
||||
className={`flex min-w-0 items-center gap-2 rounded-[10px] px-2.5 py-2 text-start font-medium leading-tight transition-colors ${on ? 'bg-surface-card text-content shadow-sm ring-1 ring-inset ring-edge-faint' : 'bg-surface-tertiary text-content-muted hover:text-content'}`}
|
||||
style={fs(11.5, 'body')}
|
||||
>
|
||||
<Icon size={13} strokeWidth={2.2} className="flex-none" aria-hidden="true" />
|
||||
<span className="min-w-0">{t(`tours.walk.theme.${theme}`)}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -313,28 +414,13 @@ export function TourPoiSearchCard({ planner }: { planner: TourPlannerController
|
||||
</p>
|
||||
{search.themes.length > 0 && (
|
||||
<div className="flex flex-col gap-1.5" data-testid="tour-poi-themes">
|
||||
<Segmented<string>
|
||||
fill
|
||||
value={search.theme ?? ''}
|
||||
onChange={(key) => search.chooseTheme(key as WalkTheme)}
|
||||
label={t('tours.walk.themes')}
|
||||
options={search.themes.map((key) => ({ value: key, label: t(`tours.walk.theme.${key}`) }))}
|
||||
/>
|
||||
<ThemeGrid themes={search.themes} active={search.theme} onChoose={search.chooseTheme} />
|
||||
<span className="text-content-faint" style={fs(11, 'body')}>
|
||||
{search.theme ? t(`tours.walk.themeHint.${search.theme}`) : t('tours.walk.themeHint.own')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-1.5" role="group" aria-label={t('tours.poi.categories')}>
|
||||
{search.offered.map((category) => (
|
||||
<CategoryChip
|
||||
key={category}
|
||||
category={category}
|
||||
on={search.categories.includes(category)}
|
||||
onToggle={() => search.toggleCategory(category)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<CategoryPicker search={search} />
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-content-muted" style={fs(11, 'body')}>
|
||||
{t('tours.poi.within')}
|
||||
@@ -381,7 +467,11 @@ export function TourPoiSearchCard({ planner }: { planner: TourPlannerController
|
||||
planner={planner}
|
||||
insertion={insertion}
|
||||
unit={unit}
|
||||
theme={search.theme && (WALK_THEMES[search.theme] as readonly string[]).includes(poi.category) ? search.theme : null}
|
||||
theme={
|
||||
search.theme && (WALK_THEMES[search.theme] as readonly string[]).includes(poi.category)
|
||||
? search.theme
|
||||
: null
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TourListItem, TourVisitStatus } from '@trek/shared';
|
||||
import { Check, CheckCheck, Redo2, SkipForward, Undo2 } from 'lucide-react';
|
||||
import { AlertTriangle, Check, CheckCheck, Clock, Redo2, SkipForward, Undo2 } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
import { tourRepo } from '../../../repo/tourRepo';
|
||||
@@ -10,7 +10,8 @@ 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';
|
||||
import { CardAction, TourNotice } from '../tourParts';
|
||||
import { clockOf, minutesOf, stationArrivals, stationFit, type StationFit } from './walkSchedule';
|
||||
|
||||
let nextId = 0;
|
||||
const freshId = () => `station-${++nextId}`;
|
||||
@@ -81,6 +82,141 @@ function StatusActions({
|
||||
);
|
||||
}
|
||||
|
||||
/** CW-04: what the arrival at a station meets, said only where it can be known. */
|
||||
function StationHours({ fit, arrival, atLeast }: { fit: StationFit; arrival: number; atLeast: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const time = clockOf(arrival);
|
||||
const conflict =
|
||||
fit.kind === 'closed'
|
||||
? t('tours.walk.hours.closed', { arrival: time })
|
||||
: fit.kind === 'closes'
|
||||
? t('tours.walk.hours.closes', { arrival: time, leave: clockOf(fit.leave), closes: clockOf(fit.closes) })
|
||||
: null;
|
||||
return (
|
||||
<span className="flex flex-col gap-1">
|
||||
<span className="inline-flex items-center gap-1 text-content-muted" style={fs(11, 'body')}>
|
||||
<Clock size={11} strokeWidth={2.2} aria-hidden="true" />
|
||||
{t(atLeast ? 'tours.walk.arrivalAtLeast' : 'tours.walk.arrival', { time })}
|
||||
</span>
|
||||
{conflict && (
|
||||
<span
|
||||
className="inline-flex items-start gap-1 text-warning"
|
||||
style={fs(11, 'body')}
|
||||
data-testid="walk-station-conflict"
|
||||
>
|
||||
<AlertTriangle size={11} strokeWidth={2.2} className="mt-0.5 flex-none" aria-hidden="true" />
|
||||
{conflict}
|
||||
</span>
|
||||
)}
|
||||
{fit.kind === 'unread' && (
|
||||
<span className="text-content-faint" style={fs(10.5, 'body')}>
|
||||
{t('tours.walk.hours.unread', { text: fit.hours })}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** One station of the list: its number or mark, name, stay and the day's arrival, and what can be marked. */
|
||||
function StationRow({
|
||||
station,
|
||||
index,
|
||||
status,
|
||||
arrival,
|
||||
date,
|
||||
onMark,
|
||||
}: {
|
||||
station: TourPlannerWaypoint;
|
||||
index: number;
|
||||
status: TourVisitStatus | undefined;
|
||||
arrival: { arrival: number; atLeast: boolean } | null;
|
||||
date: string | null;
|
||||
onMark: ((status: TourVisitStatus | 'planned') => void) | null;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<li
|
||||
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} />
|
||||
{arrival && date && (
|
||||
<StationHours
|
||||
fit={stationFit(station.openingHours, date, arrival.arrival, station.stayMinutes ?? 0)}
|
||||
arrival={arrival.arrival}
|
||||
atLeast={arrival.atLeast}
|
||||
/>
|
||||
)}
|
||||
{status && (
|
||||
<span className="text-content-faint" style={fs(11, 'body')}>
|
||||
{t(status === 'visited' ? 'tours.walk.visit.isVisited' : 'tours.walk.visit.isSkipped')}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{onMark && <StatusActions status={status} onMark={onMark} />}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
/** The lines above the stations: loading, what is missing for arrivals, where the hours come from, where to check off. */
|
||||
function WalkStationsNotes({
|
||||
loading,
|
||||
any,
|
||||
noStart,
|
||||
hoursShown,
|
||||
fromDay,
|
||||
}: {
|
||||
loading: boolean;
|
||||
any: boolean;
|
||||
noStart: boolean;
|
||||
hoursShown: boolean;
|
||||
fromDay: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const quiet = (text: string) => (
|
||||
<span className="text-content-faint" style={fs(11, 'body')}>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
if (loading) return quiet(t('common.loading'));
|
||||
return (
|
||||
<>
|
||||
{any && noStart && quiet(t('tours.walk.hours.noStart'))}
|
||||
{hoursShown && <TourNotice icon={<Clock size={13} strokeWidth={2} />}>{t('tours.walk.hours.source')}</TourNotice>}
|
||||
{any && fromDay && quiet(t('tours.walk.visit.fromDay'))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** CW-04: what the stations are checked against: the day, its start and the route. */
|
||||
export interface WalkDay {
|
||||
/** `YYYY-MM-DD`; null on a day without a date. */
|
||||
date: string | null;
|
||||
/** `HH:MM` the walk starts that day; null when none is set. */
|
||||
start: string | null;
|
||||
route: ReadonlyArray<readonly [number, number]>;
|
||||
walkingMinutes: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -91,12 +227,15 @@ export function WalkStationsCard({
|
||||
tripId,
|
||||
assignmentId,
|
||||
canMark,
|
||||
day = null,
|
||||
}: {
|
||||
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;
|
||||
/** CW-04: the day it was opened from, for arrivals and opening hours. */
|
||||
day?: WalkDay | null;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
@@ -108,9 +247,11 @@ export function WalkStationsCard({
|
||||
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;
|
||||
const start = minutesOf(day?.start);
|
||||
const arrivals = stations && day?.date ? stationArrivals(stations, day.route, start, day.walkingMinutes) : null;
|
||||
const anyHours = stations?.some((station) => station.openingHours) === true;
|
||||
const visited = (stations ?? []).filter((station) => station.key && visits?.[station.key] === 'visited').length;
|
||||
const canCheck = canMark && assignmentId !== null;
|
||||
return (
|
||||
<DialogSection
|
||||
className="flex-none"
|
||||
@@ -122,57 +263,25 @@ export function WalkStationsCard({
|
||||
}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
<WalkStationsNotes
|
||||
loading={stations === null}
|
||||
any={(stations?.length ?? 0) > 0}
|
||||
noStart={!!day && start === null}
|
||||
hoursShown={!!arrivals && anyHours}
|
||||
fromDay={assignmentId === null}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
{(stations ?? []).map((station, index) => (
|
||||
<StationRow
|
||||
key={station.id}
|
||||
station={station}
|
||||
index={index}
|
||||
status={station.key ? visits?.[station.key] : undefined}
|
||||
arrival={arrivals?.[index] ?? null}
|
||||
date={day?.date ?? null}
|
||||
onMark={canCheck && station.key ? (next) => onMark(station.key ?? '', next) : null}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</DialogSection>
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import type { TourPlannerWaypoint } from '../planner/tourPlannerWaypoints';
|
||||
|
||||
/**
|
||||
* CW-04: a city walk's stations against their opening hours on one day. The
|
||||
* arrival is an estimate: the start of that day's walk, the walking time
|
||||
* shared out along the route by distance, and the stays before. Hours are
|
||||
* read only in OpenStreetMap's plain forms ("Mo-Fr 09:00-18:00; Sa 10:00-14:00;
|
||||
* Su off", "24/7"); anything else stays unread rather than guessed, and a
|
||||
* station without hours is unknown, never open.
|
||||
*/
|
||||
|
||||
const DAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'] as const;
|
||||
|
||||
/** Minutes after midnight of a `HH:MM` time; null for anything else. */
|
||||
export function minutesOf(time: string | null | undefined): number | null {
|
||||
const match = /^(\d{1,2}):(\d{2})/.exec(time ?? '');
|
||||
if (!match) return null;
|
||||
const hours = Number(match[1]);
|
||||
const minutes = Number(match[2]);
|
||||
return hours < 24 && minutes < 60 ? hours * 60 + minutes : null;
|
||||
}
|
||||
|
||||
export function clockOf(minutes: number): string {
|
||||
const day = ((Math.round(minutes) % 1440) + 1440) % 1440;
|
||||
return `${String(Math.floor(day / 60)).padStart(2, '0')}:${String(day % 60).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/** Monday 0 to Sunday 6, of a `YYYY-MM-DD` date read as a calendar day. */
|
||||
function weekdayOf(date: string): number | null {
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
|
||||
if (!match) return null;
|
||||
const day = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))).getUTCDay();
|
||||
return (day + 6) % 7;
|
||||
}
|
||||
|
||||
function daysOf(spec: string): Set<number> | null {
|
||||
const days = new Set<number>();
|
||||
for (const part of spec.split(',')) {
|
||||
const [from, to] = part.split('-');
|
||||
const start = DAYS.indexOf(from as (typeof DAYS)[number]);
|
||||
const end = to === undefined ? start : DAYS.indexOf(to as (typeof DAYS)[number]);
|
||||
if (start < 0 || end < 0) return null;
|
||||
for (let day = start; ; day = (day + 1) % 7) {
|
||||
days.add(day);
|
||||
if (day === end) break;
|
||||
}
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
/** The windows a place is open on a date, in minutes; null where the hours cannot be read. */
|
||||
export function openWindows(hours: string, date: string): Array<[number, number]> | null {
|
||||
const weekday = weekdayOf(date);
|
||||
if (weekday === null) return null;
|
||||
const text = hours.trim();
|
||||
if (text === '24/7') return [[0, 1440]];
|
||||
let windows: Array<[number, number]> = [];
|
||||
for (const raw of text.split(';')) {
|
||||
const rule = raw.trim();
|
||||
if (!rule) continue;
|
||||
const match = /^([A-Za-z,-]+)\s+(.+)$/.exec(rule);
|
||||
if (!match) return null;
|
||||
const days = daysOf(match[1]);
|
||||
if (!days) return null;
|
||||
if (!days.has(weekday)) continue;
|
||||
// A later rule for the day replaces an earlier one, as OpenStreetMap reads them.
|
||||
if (match[2].trim() === 'off' || match[2].trim() === 'closed') {
|
||||
windows = [];
|
||||
continue;
|
||||
}
|
||||
const ranges: Array<[number, number]> = [];
|
||||
for (const span of match[2].split(',')) {
|
||||
const [open, close] = span.trim().split('-');
|
||||
const from = minutesOf(open);
|
||||
const to = close === '24:00' ? 1440 : minutesOf(close);
|
||||
if (from === null || to === null || to <= from) return null;
|
||||
ranges.push([from, to]);
|
||||
}
|
||||
windows = ranges;
|
||||
}
|
||||
return windows;
|
||||
}
|
||||
|
||||
export type StationFit =
|
||||
| { kind: 'unknown' }
|
||||
| { kind: 'unread'; hours: string }
|
||||
| { kind: 'ok' }
|
||||
| { kind: 'closed'; arrival: number }
|
||||
| { kind: 'closes'; arrival: number; leave: number; closes: number };
|
||||
|
||||
/** Whether a station is open from its arrival to the end of its stay on that date. */
|
||||
export function stationFit(hours: string | null | undefined, date: string, arrival: number, stay: number): StationFit {
|
||||
if (!hours) return { kind: 'unknown' };
|
||||
const windows = openWindows(hours, date);
|
||||
if (windows === null) return { kind: 'unread', hours };
|
||||
const leave = arrival + stay;
|
||||
const open = windows.find(([from, to]) => arrival >= from && arrival < to);
|
||||
if (!open) return { kind: 'closed', arrival };
|
||||
if (leave > open[1]) return { kind: 'closes', arrival, leave, closes: open[1] };
|
||||
return { kind: 'ok' };
|
||||
}
|
||||
|
||||
/** Metres between two points, close enough along a city walk. */
|
||||
function metres(a: readonly [number, number], b: readonly [number, number]): number {
|
||||
const rad = Math.PI / 180;
|
||||
const x = (b[1] - a[1]) * rad * Math.cos(((a[0] + b[0]) / 2) * rad);
|
||||
const y = (b[0] - a[0]) * rad;
|
||||
return Math.hypot(x, y) * 6_371_000;
|
||||
}
|
||||
|
||||
/**
|
||||
* The estimated arrival at each station, in minutes after midnight: the start,
|
||||
* the walking time shared out by how far along the route the station lies,
|
||||
* and the stays before it; a stay not known counts as nothing before it and
|
||||
* makes every later arrival a lower bound. Null when the start, the walking
|
||||
* time or the route is not known.
|
||||
*/
|
||||
export function stationArrivals(
|
||||
stations: readonly TourPlannerWaypoint[],
|
||||
route: ReadonlyArray<readonly [number, number]>,
|
||||
start: number | null,
|
||||
walkingMinutes: number | null
|
||||
): Array<{ arrival: number; atLeast: boolean }> | null {
|
||||
if (start === null || walkingMinutes === null || route.length < 2) return null;
|
||||
const along: number[] = [0];
|
||||
for (let i = 1; i < route.length; i += 1) along.push(along[i - 1] + metres(route[i - 1], route[i]));
|
||||
const total = along[along.length - 1] || 1;
|
||||
let stays = 0;
|
||||
let unknown = false;
|
||||
return stations.map((station) => {
|
||||
let nearest = 0;
|
||||
let best = Infinity;
|
||||
route.forEach((point, index) => {
|
||||
const gap = metres(point, [station.lat, station.lng]);
|
||||
if (gap < best) {
|
||||
best = gap;
|
||||
nearest = index;
|
||||
}
|
||||
});
|
||||
const arrival = start + walkingMinutes * (along[nearest] / total) + stays;
|
||||
const result = { arrival, atLeast: unknown };
|
||||
if (station.stayMinutes == null) unknown = true;
|
||||
else stays += station.stayMinutes;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
@@ -119,7 +119,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.discardChanges': 'تجاهل التغييرات',
|
||||
'tours.planner.newTourConfirmTitle': 'هل تريد بدء جولة جديدة؟',
|
||||
'tours.planner.newTourConfirmBody': 'ستُحذف تغييراتك غير المحفوظة على هذه الجولة. لن تتغيّر الجولات المحفوظة.',
|
||||
'tours.planner.newTourConfirm': 'تجاهل التغييرات وبدء جولة جديدة',
|
||||
'tours.planner.newTourConfirm': 'تجاهل وابدأ',
|
||||
'tours.planner.collapseElevation': 'طيّ مخطط الارتفاع',
|
||||
'tours.planner.expandElevation': 'توسيع مخطط الارتفاع',
|
||||
'tours.planner.name': 'اسم الجولة',
|
||||
@@ -520,6 +520,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': 'قليل من كل شيء: معالم ومتاحف وحدائق وإطلالات ومقاهٍ وأسواق.',
|
||||
'tours.walk.themeHint.own': 'اختيارك الخاص من الفئات أدناه؛ يضبطها الموضوع لك.',
|
||||
'tours.walk.themeReason': 'مقترح لموضوع {theme}',
|
||||
'tours.walk.arrival': 'الوصول نحو {time}',
|
||||
'tours.walk.arrivalAtLeast': 'الوصول في {time} على أقرب تقدير',
|
||||
'tours.walk.hours.closed': 'مغلق في {arrival} حسب ساعات العمل',
|
||||
'tours.walk.hours.closes': 'الوصول {arrival}، البقاء حتى {leave}، لكنه يغلق في {closes}',
|
||||
'tours.walk.hours.unread': 'لم تُقرأ ساعات العمل هنا: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'ساعات العمل كما في OpenStreetMap مقابل وقت الوصول التقديري؛ تحقق قبل الذهاب. المحطة بلا ساعات غير معروفة، لا مفتوحة.',
|
||||
'tours.walk.hours.noStart': 'حدد وقتًا للجولة في هذا اليوم لرؤية أوقات الوصول وساعات العمل.',
|
||||
'tours.poi.categoriesNone': 'لم تُختر أي فئة',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'نوع الجولة',
|
||||
'tours.paddle.boat': 'القارب',
|
||||
'tours.paddle.boat.kayak': 'كاياك',
|
||||
|
||||
@@ -106,7 +106,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': 'Yeni tur başlansın?',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
'Bu turda saxlanılmamış dəyişiklikləriniz ləğv ediləcək. Saxlanılmış turlar dəyişməyəcək.',
|
||||
'tours.planner.newTourConfirm': 'Dəyişiklikləri ləğv et və yeni tur başlat',
|
||||
'tours.planner.newTourConfirm': 'Ləğv et və başla',
|
||||
'tours.planner.collapseElevation': 'Yüksəklik profilini yığ',
|
||||
'tours.planner.expandElevation': 'Yüksəklik profilini aç',
|
||||
'tours.planner.name': 'Turun adı',
|
||||
@@ -516,6 +516,16 @@ const tours: TranslationStrings = {
|
||||
'Hər şeydən bir az: görməli yerlər, muzeylər, parklar, mənzərələr, kafelər və bazarlar.',
|
||||
'tours.walk.themeHint.own': 'Aşağıda öz kateqoriya seçimin; mövzu onları sənin üçün qurur.',
|
||||
'tours.walk.themeReason': '{theme} üçün təklif olunur',
|
||||
'tours.walk.arrival': 'Təxmini çatma {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Ən tez {time} çatma',
|
||||
'tours.walk.hours.closed': 'Açılış saatlarına görə {arrival} vaxtı bağlıdır',
|
||||
'tours.walk.hours.closes': 'Çatma {arrival}, qalma {leave} qədər, amma {closes} vaxtı bağlanır',
|
||||
'tours.walk.hours.unread': 'Açılış saatları burada oxunmadı: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Açılış saatları OpenStreetMap-dakı kimi, təxmini çatma vaxtı ilə; getməzdən əvvəl yoxla. Saatı olmayan dayanacaq məlum deyil, açıq deyil.',
|
||||
'tours.walk.hours.noStart': 'Çatma vaxtlarını və açılış saatlarını görmək üçün gəzintiyə bu gün vaxt ver.',
|
||||
'tours.poi.categoriesNone': 'Kateqoriya seçilməyib',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Tur növü',
|
||||
'tours.paddle.boat': 'Qayıq',
|
||||
'tours.paddle.boat.kayak': 'Kayak', // same-as-en
|
||||
|
||||
@@ -110,7 +110,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': 'Começar um novo passeio?',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
'As alterações não salvas deste passeio serão descartadas. Os passeios salvos não serão alterados.',
|
||||
'tours.planner.newTourConfirm': 'Descartar e começar novo passeio',
|
||||
'tours.planner.newTourConfirm': 'Descartar e começar',
|
||||
'tours.planner.collapseElevation': 'Recolher perfil de elevação',
|
||||
'tours.planner.expandElevation': 'Expandir perfil de elevação',
|
||||
'tours.planner.name': 'Nome do passeio',
|
||||
@@ -521,6 +521,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': 'Um pouco de cada: pontos turísticos, museus, parques, vistas, cafeterias e feiras.',
|
||||
'tours.walk.themeHint.own': 'Sua própria seleção de categorias abaixo; um tema as define para você.',
|
||||
'tours.walk.themeReason': 'Sugerido para {theme}',
|
||||
'tours.walk.arrival': 'Chegada por volta de {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Chegada a partir de {time}',
|
||||
'tours.walk.hours.closed': 'Fechado às {arrival} segundo o horário',
|
||||
'tours.walk.hours.closes': 'Chegada {arrival}, permanência até {leave}, mas fecha às {closes}',
|
||||
'tours.walk.hours.unread': 'Horário não lido aqui: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Horário como está no OpenStreetMap, comparado à chegada estimada; confira antes de ir. Uma parada sem horário é desconhecida, não aberta.',
|
||||
'tours.walk.hours.noStart': 'Dê ao passeio um horário neste dia para ver chegadas e horários.',
|
||||
'tours.poi.categoriesNone': 'Nenhuma categoria escolhida',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Tipo de passeio',
|
||||
'tours.paddle.boat': 'Embarcação',
|
||||
'tours.paddle.boat.kayak': 'Caiaque',
|
||||
|
||||
@@ -110,7 +110,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': 'Vols començar una nova excursió?',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
"Es descartaran els canvis sense desar d'aquesta excursió. Les excursions desades no es modificaran.",
|
||||
'tours.planner.newTourConfirm': 'Descarta i comença una nova excursió',
|
||||
'tours.planner.newTourConfirm': 'Descarta i comença',
|
||||
'tours.planner.collapseElevation': "Replega el perfil d'altitud",
|
||||
'tours.planner.expandElevation': "Desplega el perfil d'altitud",
|
||||
'tours.planner.name': "Nom de l'excursió",
|
||||
@@ -522,6 +522,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': "Una mica de cada: llocs d'interès, museus, parcs, vistes, cafeteries i mercats.",
|
||||
'tours.walk.themeHint.own': 'La teva pròpia tria de categories a sota; un tema te les defineix.',
|
||||
'tours.walk.themeReason': 'Suggerit per a {theme}',
|
||||
'tours.walk.arrival': 'Arribada cap a les {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Arribada com a aviat a les {time}',
|
||||
'tours.walk.hours.closed': "Tancat a les {arrival} segons l'horari",
|
||||
'tours.walk.hours.closes': 'Arribada {arrival}, estada fins a les {leave}, però tanca a les {closes}',
|
||||
'tours.walk.hours.unread': 'Horari no llegit aquí: {text}',
|
||||
'tours.walk.hours.source':
|
||||
"Horari tal com el té OpenStreetMap, contra l'arribada estimada; comprova-ho abans d'anar-hi. Una parada sense horari és desconeguda, no oberta.",
|
||||
'tours.walk.hours.noStart': 'Posa una hora a la ruta aquest dia per veure arribades i horaris.',
|
||||
'tours.poi.categoriesNone': 'Cap categoria triada',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Tipus de ruta',
|
||||
'tours.paddle.boat': 'Embarcació',
|
||||
'tours.paddle.boat.kayak': 'Caiac',
|
||||
|
||||
@@ -109,7 +109,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.discardChanges': 'Zahodit změny',
|
||||
'tours.planner.newTourConfirmTitle': 'Začít nový výlet?',
|
||||
'tours.planner.newTourConfirmBody': 'Neuložené změny tohoto výletu budou zahozeny. Uložené výlety se nezmění.',
|
||||
'tours.planner.newTourConfirm': 'Zahodit změny a začít nový výlet',
|
||||
'tours.planner.newTourConfirm': 'Zahodit a začít',
|
||||
'tours.planner.collapseElevation': 'Sbalit výškový profil',
|
||||
'tours.planner.expandElevation': 'Rozbalit výškový profil',
|
||||
'tours.planner.name': 'Název výletu',
|
||||
@@ -512,6 +512,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': 'Od všeho trochu: památky, muzea, parky, výhledy, kavárny a trhy.',
|
||||
'tours.walk.themeHint.own': 'Tvůj vlastní výběr kategorií níže; téma je nastaví za tebe.',
|
||||
'tours.walk.themeReason': 'Navrženo pro {theme}',
|
||||
'tours.walk.arrival': 'Příchod kolem {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Příchod nejdříve {time}',
|
||||
'tours.walk.hours.closed': 'V {arrival} podle otevírací doby zavřeno',
|
||||
'tours.walk.hours.closes': 'Příchod {arrival}, pobyt do {leave}, ale zavírá v {closes}',
|
||||
'tours.walk.hours.unread': 'Otevírací dobu zde nelze vyhodnotit: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Otevírací doba podle OpenStreetMap proti odhadovanému příchodu; před cestou si ji ověř. Zastávka bez časů je neznámá, ne otevřená.',
|
||||
'tours.walk.hours.noStart': 'Dej procházce v tento den čas, abys viděl příchody a otevírací doby.',
|
||||
'tours.poi.categoriesNone': 'Žádná kategorie',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Typ túry',
|
||||
'tours.paddle.boat': 'Loď',
|
||||
'tours.paddle.boat.kayak': 'Kajak',
|
||||
|
||||
@@ -111,7 +111,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': 'Neue Tour beginnen?',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
'Ungespeicherte Änderungen an dieser Tour gehen verloren. Gespeicherte Touren bleiben erhalten.',
|
||||
'tours.planner.newTourConfirm': 'Änderungen verwerfen und neue Tour beginnen',
|
||||
'tours.planner.newTourConfirm': 'Verwerfen und neu',
|
||||
'tours.planner.collapseElevation': 'Höhenprofil einklappen',
|
||||
'tours.planner.expandElevation': 'Höhenprofil ausklappen',
|
||||
'tours.planner.name': 'Name der Tour',
|
||||
@@ -527,6 +527,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': 'Von allem etwas: Sehenswürdigkeiten, Museen, Parks, Aussicht, Cafés und Märkte.',
|
||||
'tours.walk.themeHint.own': 'Deine eigene Auswahl darunter; ein Thema stellt sie für dich zusammen.',
|
||||
'tours.walk.themeReason': 'Vorgeschlagen für {theme}',
|
||||
'tours.walk.arrival': 'Ankunft ca. {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Ankunft frühestens {time}',
|
||||
'tours.walk.hours.closed': 'Um {arrival} laut Öffnungszeiten geschlossen',
|
||||
'tours.walk.hours.closes': 'Ankunft {arrival}, Aufenthalt bis {leave}, aber Schließung um {closes}',
|
||||
'tours.walk.hours.unread': 'Öffnungszeiten hier nicht auswertbar: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Öffnungszeiten wie bei OpenStreetMap eingetragen, gegen die geschätzte Ankunft; vor Ort lieber prüfen. Eine Station ohne Zeiten ist unbekannt, nicht geöffnet.',
|
||||
'tours.walk.hours.noStart': 'Gib dem Walk an diesem Tag eine Uhrzeit, um Ankünfte und Öffnungszeiten zu sehen.',
|
||||
'tours.poi.categoriesNone': 'Keine Kategorie gewählt',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Tourtyp',
|
||||
'tours.paddle.boat': 'Boot',
|
||||
'tours.paddle.boat.kayak': 'Kajak',
|
||||
|
||||
@@ -106,7 +106,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': 'Start a new tour?',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
'Your unsaved changes to this tour will be discarded. Saved tours will not be changed.',
|
||||
'tours.planner.newTourConfirm': 'Discard changes and start new tour',
|
||||
'tours.planner.newTourConfirm': 'Discard and start',
|
||||
'tours.planner.collapseElevation': 'Collapse elevation profile',
|
||||
'tours.planner.expandElevation': 'Expand elevation profile',
|
||||
'tours.planner.name': 'Tour name',
|
||||
@@ -513,6 +513,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': 'Some of each: sights, museums, parks, views, coffee shops and markets.',
|
||||
'tours.walk.themeHint.own': 'Your own pick of categories below; a theme sets them for you.',
|
||||
'tours.walk.themeReason': 'Suggested for {theme}',
|
||||
'tours.walk.arrival': 'Arrival about {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Arrival {time} at the earliest',
|
||||
'tours.walk.hours.closed': 'Closed at {arrival} by its opening hours',
|
||||
'tours.walk.hours.closes': 'Arrival {arrival}, stay until {leave}, but it closes at {closes}',
|
||||
'tours.walk.hours.unread': 'Opening hours not read here: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Opening hours as OpenStreetMap has them, against the estimated arrival; check before you go. A stop without hours is unknown, not open.',
|
||||
'tours.walk.hours.noStart': 'Give the walk a time on this day to see arrivals and opening hours.',
|
||||
'tours.poi.categoriesNone': 'No category chosen',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Tour type',
|
||||
'tours.paddle.boat': 'Boat',
|
||||
'tours.paddle.boat.kayak': 'Kayak',
|
||||
|
||||
@@ -111,7 +111,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': '¿Empezar una nueva excursión?',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
'Se descartarán los cambios sin guardar de esta excursión. Las excursiones guardadas no se modificarán.',
|
||||
'tours.planner.newTourConfirm': 'Descartar y empezar nueva excursión',
|
||||
'tours.planner.newTourConfirm': 'Descartar y empezar',
|
||||
'tours.planner.collapseElevation': 'Contraer perfil de altitud',
|
||||
'tours.planner.expandElevation': 'Expandir perfil de altitud',
|
||||
'tours.planner.name': 'Nombre de la excursión',
|
||||
@@ -520,6 +520,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': 'Un poco de cada: lugares de interés, museos, parques, vistas, cafeterías y mercados.',
|
||||
'tours.walk.themeHint.own': 'Tu propia selección de categorías abajo; un tema las elige por ti.',
|
||||
'tours.walk.themeReason': 'Sugerido para {theme}',
|
||||
'tours.walk.arrival': 'Llegada hacia las {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Llegada no antes de las {time}',
|
||||
'tours.walk.hours.closed': 'Cerrado a las {arrival} según su horario',
|
||||
'tours.walk.hours.closes': 'Llegada {arrival}, estancia hasta las {leave}, pero cierra a las {closes}',
|
||||
'tours.walk.hours.unread': 'Horario no interpretado aquí: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Horario tal como figura en OpenStreetMap, frente a la llegada estimada; compruébalo antes de ir. Una parada sin horario es desconocida, no abierta.',
|
||||
'tours.walk.hours.noStart': 'Pon al paseo una hora en este día para ver llegadas y horarios.',
|
||||
'tours.poi.categoriesNone': 'Ninguna categoría elegida',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Tipo de ruta',
|
||||
'tours.paddle.boat': 'Embarcación',
|
||||
'tours.paddle.boat.kayak': 'Kayak', // same-as-en
|
||||
|
||||
@@ -107,7 +107,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': 'Kas alustada uut retke?',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
'Selle retke salvestamata muudatused lähevad kaotsi. Salvestatud retki ei muudeta.',
|
||||
'tours.planner.newTourConfirm': 'Loobu muudatustest ja alusta uut retke',
|
||||
'tours.planner.newTourConfirm': 'Loobu ja alusta',
|
||||
'tours.planner.collapseElevation': 'Ahenda kõrgusprofiil',
|
||||
'tours.planner.expandElevation': 'Laienda kõrgusprofiil',
|
||||
'tours.planner.name': 'Retke nimi',
|
||||
@@ -515,6 +515,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': 'Igaühest natuke: vaatamisväärsused, muuseumid, pargid, vaated, kohvikud ja turud.',
|
||||
'tours.walk.themeHint.own': 'Sinu enda kategooriate valik allpool; teema seab need sinu eest.',
|
||||
'tours.walk.themeReason': 'Soovitatud teemale {theme}',
|
||||
'tours.walk.arrival': 'Saabumine umbes {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Saabumine kõige varem {time}',
|
||||
'tours.walk.hours.closed': 'Lahtiolekuaegade järgi kell {arrival} suletud',
|
||||
'tours.walk.hours.closes': 'Saabumine {arrival}, peatus kuni {leave}, aga suletakse kell {closes}',
|
||||
'tours.walk.hours.unread': 'Lahtiolekuaegu siin ei loetud: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Lahtiolekuajad nagu OpenStreetMapis, võrreldes hinnangulise saabumisega; kontrolli enne minekut. Aegadeta peatus on teadmata, mitte avatud.',
|
||||
'tours.walk.hours.noStart': 'Anna jalutusele sel päeval kellaaeg, et näha saabumisi ja lahtiolekuaegu.',
|
||||
'tours.poi.categoriesNone': 'Kategooriat pole valitud',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Matka tüüp',
|
||||
'tours.paddle.boat': 'Paat',
|
||||
'tours.paddle.boat.kayak': 'Süst',
|
||||
|
||||
@@ -113,7 +113,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': 'Commencer une nouvelle sortie ?',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
'Les modifications non enregistrées de cette sortie seront perdues. Les sorties enregistrées resteront inchangées.',
|
||||
'tours.planner.newTourConfirm': 'Abandonner et commencer une nouvelle sortie',
|
||||
'tours.planner.newTourConfirm': 'Abandonner et commencer',
|
||||
'tours.planner.collapseElevation': 'Réduire le profil altimétrique',
|
||||
'tours.planner.expandElevation': 'Déployer le profil altimétrique',
|
||||
'tours.planner.name': 'Nom de la sortie',
|
||||
@@ -530,6 +530,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': 'Un peu de chaque : sites, musées, parcs, points de vue, cafés et marchés.',
|
||||
'tours.walk.themeHint.own': 'Ta propre sélection de catégories ci-dessous ; un thème les choisit pour toi.',
|
||||
'tours.walk.themeReason': 'Suggéré pour {theme}',
|
||||
'tours.walk.arrival': 'Arrivée vers {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Arrivée au plus tôt à {time}',
|
||||
'tours.walk.hours.closed': 'Fermé à {arrival} selon ses horaires',
|
||||
'tours.walk.hours.closes': "Arrivée {arrival}, arrêt jusqu'à {leave}, mais fermeture à {closes}",
|
||||
'tours.walk.hours.unread': 'Horaires non lus ici : {text}',
|
||||
'tours.walk.hours.source':
|
||||
"Horaires tels qu'indiqués sur OpenStreetMap, face à l'arrivée estimée ; vérifie avant d'y aller. Une étape sans horaires est inconnue, pas ouverte.",
|
||||
'tours.walk.hours.noStart': 'Donne une heure à la balade ce jour-là pour voir les arrivées et les horaires.',
|
||||
'tours.poi.categoriesNone': 'Aucune catégorie choisie',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Type de sortie',
|
||||
'tours.paddle.boat': 'Embarcation',
|
||||
'tours.paddle.boat.kayak': 'Kayak', // same-as-en
|
||||
|
||||
@@ -111,7 +111,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': 'Έναρξη νέας εκδρομής;',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
'Οι μη αποθηκευμένες αλλαγές αυτής της εκδρομής θα απορριφθούν. Οι αποθηκευμένες εκδρομές δεν θα αλλάξουν.',
|
||||
'tours.planner.newTourConfirm': 'Απόρριψη αλλαγών και έναρξη νέας εκδρομής',
|
||||
'tours.planner.newTourConfirm': 'Απόρριψη και έναρξη',
|
||||
'tours.planner.collapseElevation': 'Σύμπτυξη υψομετρικού προφίλ',
|
||||
'tours.planner.expandElevation': 'Ανάπτυξη υψομετρικού προφίλ',
|
||||
'tours.planner.name': 'Όνομα εκδρομής',
|
||||
@@ -530,6 +530,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': "Λίγο απ' όλα: αξιοθέατα, μουσεία, πάρκα, θέα, καφετέριες και αγορές.",
|
||||
'tours.walk.themeHint.own': 'Η δική σου επιλογή κατηγοριών παρακάτω· ένα θέμα τις ορίζει για σένα.',
|
||||
'tours.walk.themeReason': 'Προτείνεται για {theme}',
|
||||
'tours.walk.arrival': 'Άφιξη περίπου {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Άφιξη το νωρίτερο {time}',
|
||||
'tours.walk.hours.closed': 'Κλειστό στις {arrival} σύμφωνα με το ωράριο',
|
||||
'tours.walk.hours.closes': 'Άφιξη {arrival}, παραμονή έως {leave}, αλλά κλείνει στις {closes}',
|
||||
'tours.walk.hours.unread': 'Το ωράριο δεν διαβάστηκε εδώ: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Ωράριο όπως στο OpenStreetMap, σε σχέση με την εκτιμώμενη άφιξη· έλεγξε πριν πας. Στάση χωρίς ωράριο είναι άγνωστη, όχι ανοιχτή.',
|
||||
'tours.walk.hours.noStart': 'Δώσε ώρα στη βόλτα αυτή τη μέρα για να δεις αφίξεις και ωράρια.',
|
||||
'tours.poi.categoriesNone': 'Δεν επιλέχθηκε κατηγορία',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Τύπος διαδρομής',
|
||||
'tours.paddle.boat': 'Σκάφος',
|
||||
'tours.paddle.boat.kayak': 'Καγιάκ',
|
||||
|
||||
@@ -107,7 +107,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.discardChanges': 'Módosítások elvetése',
|
||||
'tours.planner.newTourConfirmTitle': 'Új túrát kezdesz?',
|
||||
'tours.planner.newTourConfirmBody': 'A túra nem mentett módosításai elvesznek. A mentett túrák nem változnak.',
|
||||
'tours.planner.newTourConfirm': 'Módosítások elvetése és új túra kezdése',
|
||||
'tours.planner.newTourConfirm': 'Elvetés és új',
|
||||
'tours.planner.collapseElevation': 'Magassági profil összecsukása',
|
||||
'tours.planner.expandElevation': 'Magassági profil kibontása',
|
||||
'tours.planner.name': 'Túra neve',
|
||||
@@ -522,6 +522,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': 'Mindenből egy kicsit: látnivalók, múzeumok, parkok, kilátás, kávézók és piacok.',
|
||||
'tours.walk.themeHint.own': 'Saját kategóriaválasztásod lent; egy téma beállítja helyetted.',
|
||||
'tours.walk.themeReason': 'Javasolva: {theme}',
|
||||
'tours.walk.arrival': 'Érkezés kb. {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Érkezés legkorábban {time}',
|
||||
'tours.walk.hours.closed': '{arrival} órakor a nyitvatartás szerint zárva',
|
||||
'tours.walk.hours.closes': 'Érkezés {arrival}, tartózkodás {leave}-ig, de {closes}-kor zár',
|
||||
'tours.walk.hours.unread': 'A nyitvatartás itt nem értelmezhető: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Nyitvatartás az OpenStreetMap szerint, a becsült érkezéshez mérve; indulás előtt ellenőrizd. Nyitvatartás nélküli megálló ismeretlen, nem nyitott.',
|
||||
'tours.walk.hours.noStart': 'Adj időpontot a sétának ezen a napon, hogy lásd az érkezéseket és a nyitvatartást.',
|
||||
'tours.poi.categoriesNone': 'Nincs kategória kiválasztva',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Túratípus',
|
||||
'tours.paddle.boat': 'Hajó',
|
||||
'tours.paddle.boat.kayak': 'Kajak',
|
||||
|
||||
@@ -105,7 +105,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': 'Mulai tur baru?',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
'Perubahan yang belum disimpan pada tur ini akan dibuang. Tur yang sudah disimpan tidak akan diubah.',
|
||||
'tours.planner.newTourConfirm': 'Buang perubahan dan mulai tur baru',
|
||||
'tours.planner.newTourConfirm': 'Buang dan mulai',
|
||||
'tours.planner.collapseElevation': 'Ciutkan profil elevasi',
|
||||
'tours.planner.expandElevation': 'Perluas profil elevasi',
|
||||
'tours.planner.name': 'Nama tur',
|
||||
@@ -515,6 +515,16 @@ const tours: TranslationStrings = {
|
||||
'Sedikit dari semuanya: tempat wisata, museum, taman, pemandangan, kedai kopi, dan pasar.',
|
||||
'tours.walk.themeHint.own': 'Pilihan kategorimu sendiri di bawah; tema mengaturnya untukmu.',
|
||||
'tours.walk.themeReason': 'Disarankan untuk {theme}',
|
||||
'tours.walk.arrival': 'Tiba sekitar {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Tiba paling cepat {time}',
|
||||
'tours.walk.hours.closed': 'Tutup pada {arrival} menurut jam bukanya',
|
||||
'tours.walk.hours.closes': 'Tiba {arrival}, singgah sampai {leave}, tetapi tutup pukul {closes}',
|
||||
'tours.walk.hours.unread': 'Jam buka tidak terbaca di sini: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Jam buka sesuai OpenStreetMap, dibandingkan dengan perkiraan tiba; periksa sebelum berangkat. Titik tanpa jam buka tidak diketahui, bukan buka.',
|
||||
'tours.walk.hours.noStart': 'Beri rute waktu mulai di hari ini untuk melihat waktu tiba dan jam buka.',
|
||||
'tours.poi.categoriesNone': 'Belum ada kategori',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Jenis tur',
|
||||
'tours.paddle.boat': 'Perahu',
|
||||
'tours.paddle.boat.kayak': 'Kayak', // same-as-en
|
||||
|
||||
@@ -109,7 +109,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': 'Iniziare una nuova escursione?',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
'Le modifiche non salvate a questa escursione verranno scartate. Le escursioni salvate non verranno modificate.',
|
||||
'tours.planner.newTourConfirm': 'Scarta e inizia nuova escursione',
|
||||
'tours.planner.newTourConfirm': 'Scarta e inizia',
|
||||
'tours.planner.collapseElevation': 'Comprimi profilo altimetrico',
|
||||
'tours.planner.expandElevation': 'Espandi profilo altimetrico',
|
||||
'tours.planner.name': "Nome dell'escursione",
|
||||
@@ -521,6 +521,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': "Un po' di tutto: attrazioni, musei, parchi, panorami, caffetterie e mercati.",
|
||||
'tours.walk.themeHint.own': 'La tua scelta di categorie qui sotto; un tema le imposta per te.',
|
||||
'tours.walk.themeReason': 'Suggerito per {theme}',
|
||||
'tours.walk.arrival': 'Arrivo verso le {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Arrivo non prima delle {time}',
|
||||
'tours.walk.hours.closed': 'Chiuso alle {arrival} secondo gli orari',
|
||||
'tours.walk.hours.closes': 'Arrivo {arrival}, sosta fino alle {leave}, ma chiude alle {closes}',
|
||||
'tours.walk.hours.unread': 'Orari non interpretati qui: {text}',
|
||||
'tours.walk.hours.source':
|
||||
"Orari come riportati su OpenStreetMap, rispetto all'arrivo stimato; verifica prima di andare. Una tappa senza orari è sconosciuta, non aperta.",
|
||||
'tours.walk.hours.noStart': 'Dai un orario al percorso in questo giorno per vedere arrivi e orari.',
|
||||
'tours.poi.categoriesNone': 'Nessuna categoria scelta',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Tipo di tour',
|
||||
'tours.paddle.boat': 'Imbarcazione',
|
||||
'tours.paddle.boat.kayak': 'Kayak', // same-as-en
|
||||
|
||||
@@ -103,7 +103,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.discardChanges': '変更を破棄',
|
||||
'tours.planner.newTourConfirmTitle': '新しいツアーを開始しますか?',
|
||||
'tours.planner.newTourConfirmBody': 'このツアーの未保存の変更は破棄されます。保存済みのツアーは変更されません。',
|
||||
'tours.planner.newTourConfirm': '変更を破棄して新しいツアーを開始',
|
||||
'tours.planner.newTourConfirm': '破棄して開始',
|
||||
'tours.planner.collapseElevation': '標高グラフを折りたたむ',
|
||||
'tours.planner.expandElevation': '標高グラフを展開',
|
||||
'tours.planner.name': 'ツアー名',
|
||||
@@ -507,6 +507,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': '観光名所、美術館・博物館、公園、眺望、カフェ、市場を少しずつ。',
|
||||
'tours.walk.themeHint.own': '下のカテゴリーは自分で選択中。テーマを選ぶと自動で設定されます。',
|
||||
'tours.walk.themeReason': '{theme} のおすすめ',
|
||||
'tours.walk.arrival': '到着 {time} ごろ',
|
||||
'tours.walk.arrivalAtLeast': '到着は早くても {time}',
|
||||
'tours.walk.hours.closed': '営業時間では {arrival} は閉まっています',
|
||||
'tours.walk.hours.closes': '{arrival} 到着、{leave} まで滞在予定ですが {closes} に閉まります',
|
||||
'tours.walk.hours.unread': '営業時間はここでは読み取れません:{text}',
|
||||
'tours.walk.hours.source':
|
||||
'OpenStreetMap の営業時間を推定到着時刻と照合しています。出かける前に確認してください。営業時間のない立ち寄り先は「不明」で、営業中とはみなしません。',
|
||||
'tours.walk.hours.noStart': 'この日の開始時刻を設定すると、到着時刻と営業時間が表示されます。',
|
||||
'tours.poi.categoriesNone': 'カテゴリー未選択',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'ツアーの種類',
|
||||
'tours.paddle.boat': 'ボート',
|
||||
'tours.paddle.boat.kayak': 'カヤック',
|
||||
|
||||
@@ -101,7 +101,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': '새 투어를 시작할까요?',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
'이 투어의 저장하지 않은 변경 사항은 삭제됩니다. 저장된 투어는 변경되지 않습니다.',
|
||||
'tours.planner.newTourConfirm': '변경 사항을 버리고 새 투어 시작',
|
||||
'tours.planner.newTourConfirm': '버리고 시작',
|
||||
'tours.planner.collapseElevation': '고도 그래프 접기',
|
||||
'tours.planner.expandElevation': '고도 그래프 펼치기',
|
||||
'tours.planner.name': '투어 이름',
|
||||
@@ -502,6 +502,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': '명소, 박물관, 공원, 전망, 카페, 시장을 골고루.',
|
||||
'tours.walk.themeHint.own': '아래 카테고리는 직접 고른 것입니다. 테마를 고르면 자동으로 설정됩니다.',
|
||||
'tours.walk.themeReason': '{theme} 추천',
|
||||
'tours.walk.arrival': '도착 약 {time}',
|
||||
'tours.walk.arrivalAtLeast': '도착 빨라야 {time}',
|
||||
'tours.walk.hours.closed': '영업시간상 {arrival}에는 문을 닫습니다',
|
||||
'tours.walk.hours.closes': '{arrival} 도착, {leave}까지 머물 예정이지만 {closes}에 문을 닫습니다',
|
||||
'tours.walk.hours.unread': '여기서는 영업시간을 읽지 못했습니다: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'OpenStreetMap의 영업시간을 예상 도착 시간과 비교합니다. 가기 전에 확인하세요. 영업시간이 없는 정류지는 영업 중이 아니라 미상입니다.',
|
||||
'tours.walk.hours.noStart': '이날 산책에 시간을 지정하면 도착 시간과 영업시간을 볼 수 있습니다.',
|
||||
'tours.poi.categoriesNone': '선택한 카테고리 없음',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': '투어 유형',
|
||||
'tours.paddle.boat': '보트',
|
||||
'tours.paddle.boat.kayak': '카약',
|
||||
|
||||
@@ -111,7 +111,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': 'Een nieuwe tocht starten?',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
'Je niet-opgeslagen wijzigingen aan deze tocht worden verworpen. Opgeslagen tochten blijven ongewijzigd.',
|
||||
'tours.planner.newTourConfirm': 'Verwerpen en nieuwe tocht starten',
|
||||
'tours.planner.newTourConfirm': 'Weggooien en beginnen',
|
||||
'tours.planner.collapseElevation': 'Hoogteprofiel inklappen',
|
||||
'tours.planner.expandElevation': 'Hoogteprofiel uitklappen',
|
||||
'tours.planner.name': 'Naam van de tocht',
|
||||
@@ -525,6 +525,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': 'Van alles wat: bezienswaardigheden, musea, parken, uitzicht, koffiezaken en markten.',
|
||||
'tours.walk.themeHint.own': 'Je eigen keuze van categorieën hieronder; een thema stelt ze voor je in.',
|
||||
'tours.walk.themeReason': 'Voorgesteld voor {theme}',
|
||||
'tours.walk.arrival': 'Aankomst rond {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Aankomst op zijn vroegst {time}',
|
||||
'tours.walk.hours.closed': 'Om {arrival} gesloten volgens de openingstijden',
|
||||
'tours.walk.hours.closes': 'Aankomst {arrival}, verblijf tot {leave}, maar sluit om {closes}',
|
||||
'tours.walk.hours.unread': 'Openingstijden hier niet gelezen: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Openingstijden zoals in OpenStreetMap, tegen de geschatte aankomst; controleer ze voor je gaat. Een halte zonder tijden is onbekend, niet open.',
|
||||
'tours.walk.hours.noStart': 'Geef de wandeling op deze dag een tijd om aankomsten en openingstijden te zien.',
|
||||
'tours.poi.categoriesNone': 'Geen categorie gekozen',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Soort tocht',
|
||||
'tours.paddle.boat': 'Boot',
|
||||
'tours.paddle.boat.kayak': 'Kajak',
|
||||
|
||||
@@ -117,7 +117,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': 'Rozpocząć nową wycieczkę?',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
'Niezapisane zmiany tej wycieczki zostaną odrzucone. Zapisane wycieczki nie zostaną zmienione.',
|
||||
'tours.planner.newTourConfirm': 'Odrzuć zmiany i rozpocznij nową wycieczkę',
|
||||
'tours.planner.newTourConfirm': 'Odrzuć i zacznij',
|
||||
'tours.planner.collapseElevation': 'Zwiń profil wysokościowy',
|
||||
'tours.planner.expandElevation': 'Rozwiń profil wysokościowy',
|
||||
'tours.planner.name': 'Nazwa wycieczki',
|
||||
@@ -529,6 +529,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': 'Po trochu wszystkiego: atrakcje, muzea, parki, widoki, kawiarnie i targi.',
|
||||
'tours.walk.themeHint.own': 'Twój własny wybór kategorii poniżej; motyw ustawi je za ciebie.',
|
||||
'tours.walk.themeReason': 'Proponowane dla: {theme}',
|
||||
'tours.walk.arrival': 'Przyjście około {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Przyjście najwcześniej {time}',
|
||||
'tours.walk.hours.closed': 'Według godzin otwarcia o {arrival} zamknięte',
|
||||
'tours.walk.hours.closes': 'Przyjście {arrival}, pobyt do {leave}, ale zamknięcie o {closes}',
|
||||
'tours.walk.hours.unread': 'Godziny otwarcia nieodczytane: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Godziny otwarcia z OpenStreetMap wobec szacowanego przyjścia; sprawdź przed wyjściem. Przystanek bez godzin jest nieznany, nie otwarty.',
|
||||
'tours.walk.hours.noStart': 'Ustaw godzinę trasy tego dnia, aby zobaczyć przyjścia i godziny otwarcia.',
|
||||
'tours.poi.categoriesNone': 'Nie wybrano kategorii',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Rodzaj wycieczki',
|
||||
'tours.paddle.boat': 'Łódź',
|
||||
'tours.paddle.boat.kayak': 'Kajak',
|
||||
|
||||
@@ -118,7 +118,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': 'Начать новый тур?',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
'Несохранённые изменения этого тура будут отменены. Сохранённые туры не изменятся.',
|
||||
'tours.planner.newTourConfirm': 'Отменить изменения и начать новый тур',
|
||||
'tours.planner.newTourConfirm': 'Сбросить и начать',
|
||||
'tours.planner.collapseElevation': 'Свернуть профиль высот',
|
||||
'tours.planner.expandElevation': 'Развернуть профиль высот',
|
||||
'tours.planner.name': 'Название тура',
|
||||
@@ -532,6 +532,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': 'Всего понемногу: достопримечательности, музеи, парки, виды, кафе и рынки.',
|
||||
'tours.walk.themeHint.own': 'Твой собственный выбор категорий ниже; тема задаст их за тебя.',
|
||||
'tours.walk.themeReason': 'Предложено для темы {theme}',
|
||||
'tours.walk.arrival': 'Прибытие около {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Прибытие не раньше {time}',
|
||||
'tours.walk.hours.closed': 'По часам работы в {arrival} закрыто',
|
||||
'tours.walk.hours.closes': 'Прибытие {arrival}, остановка до {leave}, но закрывается в {closes}',
|
||||
'tours.walk.hours.unread': 'Часы работы здесь не разобраны: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Часы работы как в OpenStreetMap, относительно расчётного прибытия; проверь перед выходом. Остановка без часов неизвестна, а не открыта.',
|
||||
'tours.walk.hours.noStart': 'Задай прогулке время в этот день, чтобы видеть прибытия и часы работы.',
|
||||
'tours.poi.categoriesNone': 'Категория не выбрана',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Тип тура',
|
||||
'tours.paddle.boat': 'Лодка',
|
||||
'tours.paddle.boat.kayak': 'Каяк',
|
||||
|
||||
@@ -110,7 +110,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.discardChanges': 'Zahodiť zmeny',
|
||||
'tours.planner.newTourConfirmTitle': 'Začať novú túru?',
|
||||
'tours.planner.newTourConfirmBody': 'Neuložené zmeny tejto túry sa zahodia. Uložené túry zostanú nezmenené.',
|
||||
'tours.planner.newTourConfirm': 'Zahodiť zmeny a začať novú túru',
|
||||
'tours.planner.newTourConfirm': 'Zahodiť a začať',
|
||||
'tours.planner.collapseElevation': 'Zbaliť výškový profil',
|
||||
'tours.planner.expandElevation': 'Rozbaliť výškový profil',
|
||||
'tours.planner.name': 'Názov túry',
|
||||
@@ -514,6 +514,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': 'Od všetkého trochu: pamiatky, múzeá, parky, výhľady, kaviarne a trhy.',
|
||||
'tours.walk.themeHint.own': 'Tvoj vlastný výber kategórií nižšie; téma ich nastaví za teba.',
|
||||
'tours.walk.themeReason': 'Navrhnuté pre {theme}',
|
||||
'tours.walk.arrival': 'Príchod okolo {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Príchod najskôr {time}',
|
||||
'tours.walk.hours.closed': 'Podľa otváracích hodín o {arrival} zatvorené',
|
||||
'tours.walk.hours.closes': 'Príchod {arrival}, pobyt do {leave}, ale zatvára o {closes}',
|
||||
'tours.walk.hours.unread': 'Otváracie hodiny tu nemožno vyhodnotiť: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Otváracie hodiny podľa OpenStreetMap oproti odhadovanému príchodu; pred cestou si ich over. Zastávka bez časov je neznáma, nie otvorená.',
|
||||
'tours.walk.hours.noStart': 'Daj prechádzke v tento deň čas, aby si videl príchody a otváracie hodiny.',
|
||||
'tours.poi.categoriesNone': 'Žiadna kategória',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Typ túry',
|
||||
'tours.paddle.boat': 'Loď',
|
||||
'tours.paddle.boat.kayak': 'Kajak',
|
||||
|
||||
@@ -106,7 +106,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.discardChanges': 'Kasta ändringar',
|
||||
'tours.planner.newTourConfirmTitle': 'Börja på en ny tur?',
|
||||
'tours.planner.newTourConfirmBody': 'Dina osparade ändringar i den här turen kastas. Sparade turer ändras inte.',
|
||||
'tours.planner.newTourConfirm': 'Kasta ändringar och börja på ny tur',
|
||||
'tours.planner.newTourConfirm': 'Släng och börja',
|
||||
'tours.planner.collapseElevation': 'Fäll ihop höjdprofil',
|
||||
'tours.planner.expandElevation': 'Fäll ut höjdprofil',
|
||||
'tours.planner.name': 'Turens namn',
|
||||
@@ -513,6 +513,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': 'Lite av allt: sevärdheter, museer, parker, utsikter, kaféer och torghandel.',
|
||||
'tours.walk.themeHint.own': 'Ditt eget urval av kategorier nedan; ett tema väljer dem åt dig.',
|
||||
'tours.walk.themeReason': 'Föreslaget för {theme}',
|
||||
'tours.walk.arrival': 'Ankomst runt {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Ankomst tidigast {time}',
|
||||
'tours.walk.hours.closed': 'Stängt kl. {arrival} enligt öppettiderna',
|
||||
'tours.walk.hours.closes': 'Ankomst {arrival}, vistelse till {leave}, men stänger kl. {closes}',
|
||||
'tours.walk.hours.unread': 'Öppettiderna kunde inte läsas här: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Öppettider enligt OpenStreetMap, mot beräknad ankomst; kontrollera innan du går. Ett stopp utan tider är okänt, inte öppet.',
|
||||
'tours.walk.hours.noStart': 'Ge promenaden en tid den här dagen för att se ankomster och öppettider.',
|
||||
'tours.poi.categoriesNone': 'Ingen kategori vald',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Turtyp',
|
||||
'tours.paddle.boat': 'Båt',
|
||||
'tours.paddle.boat.kayak': 'Kajak',
|
||||
|
||||
@@ -100,7 +100,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': 'เริ่มทัวร์ใหม่ไหม?',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
'การเปลี่ยนแปลงของทัวร์นี้ที่ยังไม่บันทึกจะถูกทิ้ง ทัวร์ที่บันทึกแล้วจะไม่เปลี่ยนแปลง',
|
||||
'tours.planner.newTourConfirm': 'ทิ้งการเปลี่ยนแปลงและเริ่มทัวร์ใหม่',
|
||||
'tours.planner.newTourConfirm': 'ทิ้งแล้วเริ่มใหม่',
|
||||
'tours.planner.collapseElevation': 'ยุบกราฟระดับความสูง',
|
||||
'tours.planner.expandElevation': 'ขยายกราฟระดับความสูง',
|
||||
'tours.planner.name': 'ชื่อทัวร์',
|
||||
@@ -496,6 +496,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': 'มีทุกอย่าง: สถานที่น่าสนใจ พิพิธภัณฑ์ สวน จุดชมวิว ร้านกาแฟ และตลาด',
|
||||
'tours.walk.themeHint.own': 'หมวดหมู่ด้านล่างเลือกเอง ธีมจะตั้งค่าให้อัตโนมัติ',
|
||||
'tours.walk.themeReason': 'แนะนำสำหรับ {theme}',
|
||||
'tours.walk.arrival': 'ถึงประมาณ {time}',
|
||||
'tours.walk.arrivalAtLeast': 'ถึงเร็วที่สุด {time}',
|
||||
'tours.walk.hours.closed': 'ตามเวลาทำการ ปิดเวลา {arrival}',
|
||||
'tours.walk.hours.closes': 'ถึง {arrival} แวะถึง {leave} แต่ปิด {closes}',
|
||||
'tours.walk.hours.unread': 'อ่านเวลาทำการไม่ได้: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'เวลาทำการตาม OpenStreetMap เทียบกับเวลาถึงโดยประมาณ โปรดตรวจสอบก่อนไป จุดแวะที่ไม่มีเวลาทำการถือว่าไม่ทราบ ไม่ใช่เปิดอยู่',
|
||||
'tours.walk.hours.noStart': 'กำหนดเวลาให้ทริปในวันนี้เพื่อดูเวลาถึงและเวลาทำการ',
|
||||
'tours.poi.categoriesNone': 'ยังไม่ได้เลือกหมวดหมู่',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'ประเภททริป',
|
||||
'tours.paddle.boat': 'เรือ',
|
||||
'tours.paddle.boat.kayak': 'เรือคายัก',
|
||||
|
||||
@@ -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 = 756;
|
||||
const GENERAL_KEY_COUNT = 765;
|
||||
|
||||
function literalKeys(source: string): string[] {
|
||||
const tree = ts.createSourceFile('locale.ts', source, ts.ScriptTarget.Latest, true);
|
||||
|
||||
@@ -106,7 +106,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': 'Yeni bir tur başlatılsın mı?',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
'Bu turdaki kaydedilmemiş değişiklikler silinecek. Kaydedilmiş turlar değişmeyecek.',
|
||||
'tours.planner.newTourConfirm': 'Değişiklikleri sil ve yeni tur başlat',
|
||||
'tours.planner.newTourConfirm': 'Vazgeç ve başla',
|
||||
'tours.planner.collapseElevation': 'Yükseklik profilini daralt',
|
||||
'tours.planner.expandElevation': 'Yükseklik profilini genişlet',
|
||||
'tours.planner.name': 'Tur adı',
|
||||
@@ -517,6 +517,16 @@ const tours: TranslationStrings = {
|
||||
'Her şeyden biraz: görülecek yerler, müzeler, parklar, manzaralar, kafeler ve pazarlar.',
|
||||
'tours.walk.themeHint.own': 'Aşağıdaki kategorileri kendin seçtin; bir tema onları senin için ayarlar.',
|
||||
'tours.walk.themeReason': '{theme} için önerildi',
|
||||
'tours.walk.arrival': 'Varış yaklaşık {time}',
|
||||
'tours.walk.arrivalAtLeast': 'En erken varış {time}',
|
||||
'tours.walk.hours.closed': 'Açılış saatlerine göre {arrival} saatinde kapalı',
|
||||
'tours.walk.hours.closes': 'Varış {arrival}, kalış {leave} saatine kadar, ama {closes} saatinde kapanıyor',
|
||||
'tours.walk.hours.unread': 'Açılış saatleri burada okunamadı: {text}',
|
||||
'tours.walk.hours.source':
|
||||
"Açılış saatleri OpenStreetMap'teki gibi, tahmini varışa göre; gitmeden önce kontrol et. Saati olmayan durak bilinmiyordur, açık değildir.",
|
||||
'tours.walk.hours.noStart': 'Varışları ve açılış saatlerini görmek için yürüyüşe bu gün bir saat ver.',
|
||||
'tours.poi.categoriesNone': 'Kategori seçilmedi',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Tur türü',
|
||||
'tours.paddle.boat': 'Tekne',
|
||||
'tours.paddle.boat.kayak': 'Kano (kayak)',
|
||||
|
||||
@@ -115,7 +115,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.discardChanges': 'Скасувати зміни',
|
||||
'tours.planner.newTourConfirmTitle': 'Почати новий тур?',
|
||||
'tours.planner.newTourConfirmBody': 'Незбережені зміни цього туру буде скасовано. Збережені тури не зміняться.',
|
||||
'tours.planner.newTourConfirm': 'Скасувати зміни й почати новий тур',
|
||||
'tours.planner.newTourConfirm': 'Скинути й почати',
|
||||
'tours.planner.collapseElevation': 'Згорнути профіль висот',
|
||||
'tours.planner.expandElevation': 'Розгорнути профіль висот',
|
||||
'tours.planner.name': 'Назва туру',
|
||||
@@ -528,6 +528,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': "Усього потроху: пам'ятки, музеї, парки, краєвиди, кав'ярні та ринки.",
|
||||
'tours.walk.themeHint.own': 'Твій власний вибір категорій нижче; тема задасть їх за тебе.',
|
||||
'tours.walk.themeReason': 'Запропоновано для теми {theme}',
|
||||
'tours.walk.arrival': 'Прибуття близько {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Прибуття не раніше {time}',
|
||||
'tours.walk.hours.closed': 'За годинами роботи о {arrival} зачинено',
|
||||
'tours.walk.hours.closes': 'Прибуття {arrival}, перебування до {leave}, але зачиняється о {closes}',
|
||||
'tours.walk.hours.unread': 'Години роботи тут не розібрано: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Години роботи як в OpenStreetMap, відносно розрахункового прибуття; перевір перед виходом. Зупинка без годин невідома, а не відкрита.',
|
||||
'tours.walk.hours.noStart': 'Задай прогулянці час цього дня, щоб бачити прибуття й години роботи.',
|
||||
'tours.poi.categoriesNone': 'Категорію не вибрано',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Тип туру',
|
||||
'tours.paddle.boat': 'Човен',
|
||||
'tours.paddle.boat.kayak': 'Каяк',
|
||||
|
||||
@@ -107,7 +107,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.newTourConfirmTitle': 'Bắt đầu chuyến tham quan mới?',
|
||||
'tours.planner.newTourConfirmBody':
|
||||
'Các thay đổi chưa lưu của chuyến tham quan này sẽ bị bỏ. Các chuyến tham quan đã lưu sẽ không thay đổi.',
|
||||
'tours.planner.newTourConfirm': 'Bỏ thay đổi và bắt đầu chuyến tham quan mới',
|
||||
'tours.planner.newTourConfirm': 'Hủy và bắt đầu',
|
||||
'tours.planner.collapseElevation': 'Thu gọn biểu đồ độ cao',
|
||||
'tours.planner.expandElevation': 'Mở rộng biểu đồ độ cao',
|
||||
'tours.planner.name': 'Tên chuyến tham quan',
|
||||
@@ -513,6 +513,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': 'Mỗi thứ một chút: điểm tham quan, bảo tàng, công viên, cảnh đẹp, quán cà phê và chợ.',
|
||||
'tours.walk.themeHint.own': 'Bạn đang tự chọn danh mục bên dưới; chủ đề sẽ chọn giúp bạn.',
|
||||
'tours.walk.themeReason': 'Gợi ý cho {theme}',
|
||||
'tours.walk.arrival': 'Đến khoảng {time}',
|
||||
'tours.walk.arrivalAtLeast': 'Đến sớm nhất lúc {time}',
|
||||
'tours.walk.hours.closed': 'Theo giờ mở cửa, lúc {arrival} đóng cửa',
|
||||
'tours.walk.hours.closes': 'Đến {arrival}, ở đến {leave}, nhưng đóng cửa lúc {closes}',
|
||||
'tours.walk.hours.unread': 'Không đọc được giờ mở cửa ở đây: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Giờ mở cửa theo OpenStreetMap, so với giờ đến ước tính; hãy kiểm tra trước khi đi. Điểm dừng không có giờ là chưa rõ, không phải đang mở.',
|
||||
'tours.walk.hours.noStart': 'Đặt giờ cho lộ trình trong ngày này để xem giờ đến và giờ mở cửa.',
|
||||
'tours.poi.categoriesNone': 'Chưa chọn danh mục',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': 'Loại chuyến',
|
||||
'tours.paddle.boat': 'Thuyền',
|
||||
'tours.paddle.boat.kayak': 'Thuyền kayak',
|
||||
|
||||
@@ -96,7 +96,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.discardChanges': '捨棄變更',
|
||||
'tours.planner.newTourConfirmTitle': '開始新的遊程?',
|
||||
'tours.planner.newTourConfirmBody': '這個遊程中尚未儲存的變更將被捨棄。已儲存的遊程不會變更。',
|
||||
'tours.planner.newTourConfirm': '捨棄變更並開始新遊程',
|
||||
'tours.planner.newTourConfirm': '放棄並開始',
|
||||
'tours.planner.collapseElevation': '收合海拔剖面圖',
|
||||
'tours.planner.expandElevation': '展開海拔剖面圖',
|
||||
'tours.planner.name': '遊程名稱',
|
||||
@@ -481,6 +481,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': '各類都有一些:景點、博物館、公園、景觀、咖啡館和市集。',
|
||||
'tours.walk.themeHint.own': '下方類別為你自選;選擇主題會自動設定。',
|
||||
'tours.walk.themeReason': '推薦用於{theme}',
|
||||
'tours.walk.arrival': '約 {time} 抵達',
|
||||
'tours.walk.arrivalAtLeast': '最早 {time} 抵達',
|
||||
'tours.walk.hours.closed': '按營業時間,{arrival} 時已關門',
|
||||
'tours.walk.hours.closes': '{arrival} 抵達,停留至 {leave},但 {closes} 關門',
|
||||
'tours.walk.hours.unread': '此處無法解析營業時間:{text}',
|
||||
'tours.walk.hours.source':
|
||||
'營業時間以 OpenStreetMap 為準,並與預計抵達時間對照;出發前請再確認。沒有營業時間的站點視為未知,而非營業中。',
|
||||
'tours.walk.hours.noStart': '為當天的漫步設定時間,即可查看抵達時間和營業時間。',
|
||||
'tours.poi.categoriesNone': '未選擇類別',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': '路線類型',
|
||||
'tours.paddle.boat': '船隻',
|
||||
'tours.paddle.boat.kayak': '獨木舟(Kayak)',
|
||||
|
||||
@@ -97,7 +97,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.discardChanges': '放弃更改',
|
||||
'tours.planner.newTourConfirmTitle': '开始规划新游览路线?',
|
||||
'tours.planner.newTourConfirmBody': '此游览路线中未保存的更改将被丢弃。已保存的游览路线不会改变。',
|
||||
'tours.planner.newTourConfirm': '放弃更改并开始新游览路线',
|
||||
'tours.planner.newTourConfirm': '放弃并开始',
|
||||
'tours.planner.collapseElevation': '收起海拔剖面图',
|
||||
'tours.planner.expandElevation': '展开海拔剖面图',
|
||||
'tours.planner.name': '游览名称',
|
||||
@@ -482,6 +482,16 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.themeHint.mixed': '各类都有一些:景点、博物馆、公园、景观、咖啡馆和集市。',
|
||||
'tours.walk.themeHint.own': '下方类别为你自选;选择主题会自动设置。',
|
||||
'tours.walk.themeReason': '推荐用于{theme}',
|
||||
'tours.walk.arrival': '约 {time} 到达',
|
||||
'tours.walk.arrivalAtLeast': '最早 {time} 到达',
|
||||
'tours.walk.hours.closed': '按营业时间,{arrival} 时已关门',
|
||||
'tours.walk.hours.closes': '{arrival} 到达,停留至 {leave},但 {closes} 关门',
|
||||
'tours.walk.hours.unread': '此处无法解析营业时间:{text}',
|
||||
'tours.walk.hours.source':
|
||||
'营业时间以 OpenStreetMap 为准,并与预计到达时间对照;出发前请再确认。没有营业时间的站点视为未知,而非营业中。',
|
||||
'tours.walk.hours.noStart': '为当天的漫步设定时间,即可查看到达时间和营业时间。',
|
||||
'tours.poi.categoriesNone': '未选择类别',
|
||||
'tours.poi.categoriesMore': '{first} +{more}',
|
||||
'tours.planner.tourType': '路线类型',
|
||||
'tours.paddle.boat': '船只',
|
||||
'tours.paddle.boat.kayak': '皮划艇',
|
||||
|
||||
Reference in New Issue
Block a user