mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
fix(tours): the last review findings of the city walks
A place closing at midnight is no longer read as open past it: the hours run on only where the next date opens at midnight (any window of the day counts, as written order is kept), and an impossible date such as 2026-02-31 reads as unknown instead of rolling into March. The hours source names the trip's places rather than "your own" entries, in all 27 locales, as the pill beside it does. The day a walk's arrivals are checked for shows its date on the pill, not only "Day N". The 12 hour clock is tested for a window to midnight. The along-the-tour term table is built once again, now that the one test that stubbed the places client without its real exports spreads them. New test seeding goes through the ORM factories.
This commit is contained in:
@@ -192,7 +192,7 @@ describe('TourDetailDialog', () => {
|
||||
}
|
||||
const view = render(<TourDetailDialog {...defaultProps} tour={walk} days={days} assignments={assignments} selectedDayId={7} />)
|
||||
|
||||
expect(screen.getByTestId('walk-day')).toHaveTextContent('Day 1, start 09:00')
|
||||
expect(screen.getByTestId('walk-day')).toHaveTextContent(/^Day 1, Mon, Oct 19(, 2026)?, start 09:00$/)
|
||||
expect(screen.queryByTestId('walk-day-picker')).not.toBeInTheDocument()
|
||||
expect(await screen.findByText('Arrival about 09:00, stay until 09:30')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('walk-station-conflict'))
|
||||
@@ -200,7 +200,7 @@ describe('TourDetailDialog', () => {
|
||||
expect(screen.getByText('Mark visited')).toBeInTheDocument()
|
||||
|
||||
view.rerender(<TourDetailDialog {...defaultProps} tour={walk} days={days} assignments={assignments} />)
|
||||
expect(screen.getByTestId('walk-day-picker')).toHaveTextContent('Day 1, start 09:00')
|
||||
expect(screen.getByTestId('walk-day-picker')).toHaveTextContent(/^Day 1, Mon, Oct 19(, 2026)?, start 09:00$/)
|
||||
expect(screen.queryByText('Mark visited')).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByTestId('walk-day-picker'))
|
||||
fireEvent.click(screen.getByRole('button', { name: /^Day 2, .*start 11:00$/ }))
|
||||
|
||||
@@ -294,7 +294,7 @@ describe('TourWaypointList', () => {
|
||||
|
||||
it('FE-TOUR-WAYPOINT-LIST-009: names the day above the stops and the conflict at the stop it is about', () => {
|
||||
render(<TourWaypointList planner={walkPlanner()} canEdit isSaving={false} />);
|
||||
expect(screen.getByTestId('walk-day')).toHaveTextContent('Day 1, start 17:20');
|
||||
expect(screen.getByTestId('walk-day')).toHaveTextContent(/^Day 1, Mon, Oct 19(, 2026)?, start 17:20$/);
|
||||
const rows = screen.getAllByTestId('tour-planner-waypoint-row');
|
||||
const first = within(at(rows, 0)).getByTestId('walk-station-schedule');
|
||||
expect(within(first).getByText('Arrival about 17:20, stay until 18:20')).toBeInTheDocument();
|
||||
@@ -312,7 +312,7 @@ describe('TourWaypointList', () => {
|
||||
|
||||
it('FE-TOUR-WAYPOINT-LIST-010: shows no arrival while the route is not planned for the points as they are', () => {
|
||||
render(<TourWaypointList planner={walkPlanner({ routeCurrent: false })} canEdit isSaving={false} />);
|
||||
expect(screen.getByTestId('walk-day')).toHaveTextContent('Day 1, start 17:20');
|
||||
expect(screen.getByTestId('walk-day')).toHaveTextContent(/^Day 1, Mon, Oct 19(, 2026)?, start 17:20$/);
|
||||
expect(screen.queryByTestId('walk-station-schedule')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('walk-station-conflict')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -36,12 +36,16 @@ describe('StationScheduleLine', () => {
|
||||
|
||||
it('follows the 12 hour format, a window to midnight included', () => {
|
||||
seedStore(useSettingsStore, { settings: { time_format: '12h' } });
|
||||
line(at({ leave: 1130, fit: { kind: 'closes', source: 'place', window: [600, 1080] } }));
|
||||
const { unmount } = line(at({ leave: 1130, fit: { kind: 'closes', source: 'place', window: [600, 1080] } }));
|
||||
expect(screen.getByText('Arrival about 5:20 PM, stay until 6:50 PM')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('walk-station-conflict')).toHaveTextContent(
|
||||
'Lenbachhaus closes at 6:00 PM, during the stay from 5:20 PM to 6:50 PM (open 10:00 AM to 6:00 PM)'
|
||||
);
|
||||
expect(screen.getByTestId('walk-hours-source')).toHaveTextContent('Trip place');
|
||||
// In 12 hours a window to midnight ends at 12:00 AM, never at 24:00.
|
||||
unmount();
|
||||
line(at({ fit: { kind: 'open', source: 'place', window: [600, 1440] } }));
|
||||
expect(screen.getByText(/Open 10:00 AM to 12:00 AM/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the window an open station is checked against; one to midnight reads 24:00', () => {
|
||||
|
||||
@@ -44,7 +44,8 @@ describe('WalkDayLine', () => {
|
||||
render(<Picker />);
|
||||
expect(screen.getByText('Times for')).toBeInTheDocument();
|
||||
const picker = screen.getByTestId('walk-day-picker');
|
||||
expect(picker).toHaveTextContent('Day 2, start 09:30');
|
||||
// The pill names the date the hours are for, as the menu does.
|
||||
expect(picker).toHaveTextContent(/^Day 2, Tue, Oct 20(, 2026)?, start 09:30$/);
|
||||
expect(picker).toHaveAttribute('aria-expanded', 'false');
|
||||
|
||||
fireEvent.click(picker);
|
||||
@@ -56,7 +57,9 @@ describe('WalkDayLine', () => {
|
||||
]);
|
||||
expect(items.map((item) => !!item.querySelector('svg'))).toEqual([false, true]);
|
||||
fireEvent.click(screen.getByRole('button', { name: /^Day 1, / }));
|
||||
expect(screen.getByTestId('walk-day-picker')).toHaveTextContent('Day 1, no start time');
|
||||
expect(screen.getByTestId('walk-day-picker')).toHaveTextContent(
|
||||
/^Day 1, Mon, Oct 19(, 2026)?: Old town, no start time$/
|
||||
);
|
||||
});
|
||||
|
||||
it('closes the menu again from its pill', () => {
|
||||
@@ -72,7 +75,7 @@ describe('WalkDayLine', () => {
|
||||
const { rerender } = render(
|
||||
<WalkDayLine choice={choice({ fixed: true })} schedule={schedule('ready')} stationCount={3} />
|
||||
);
|
||||
expect(screen.getByTestId('walk-day')).toHaveTextContent('Day 2, start 09:30');
|
||||
expect(screen.getByTestId('walk-day')).toHaveTextContent(/^Day 2, Tue, Oct 20(, 2026)?, start 09:30$/);
|
||||
expect(screen.queryByTestId('walk-day-picker')).not.toBeInTheDocument();
|
||||
rerender(
|
||||
<WalkDayLine
|
||||
@@ -81,7 +84,7 @@ describe('WalkDayLine', () => {
|
||||
stationCount={3}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('walk-day')).toHaveTextContent('Day 1, no start time');
|
||||
expect(screen.getByTestId('walk-day')).toHaveTextContent(/^Day 1, .+: Old town, no start time$/);
|
||||
expect(screen.getByText('Give the walk a time on this day to see arrivals and opening hours.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -102,7 +105,7 @@ describe('WalkDayLine', () => {
|
||||
const { rerender, container } = render(
|
||||
<WalkDayLine choice={choice()} schedule={schedule('ready', true)} stationCount={3} />
|
||||
);
|
||||
expect(screen.getByText(/Opening hours from OpenStreetMap or your own place entries/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Opening hours from OpenStreetMap or the trip's places/)).toBeInTheDocument();
|
||||
rerender(<WalkDayLine choice={choice()} schedule={schedule('noRoute', true)} stationCount={3} />);
|
||||
expect(screen.queryByText(/Opening hours from OpenStreetMap/)).not.toBeInTheDocument();
|
||||
rerender(<WalkDayLine enabled={false} choice={choice()} schedule={schedule('ready', true)} stationCount={3} />);
|
||||
|
||||
@@ -15,7 +15,7 @@ type Translate = ReturnType<typeof useTranslation>['t'];
|
||||
const PILL =
|
||||
'inline-flex min-w-0 items-center gap-1 rounded-full bg-surface-card px-2 py-0.5 text-start font-medium text-content-secondary shadow-sm';
|
||||
|
||||
/** "Day 2, start 09:00", or that it has no start time; `day` names the day as the place needs it. */
|
||||
/** "Day 2, Tue, Jul 8, start 09:00", or that it has no start time; `day` names the day with its date. */
|
||||
function optionLabel(option: WalkDayOption, day: string, t: Translate, is12h: boolean): string {
|
||||
return option.start
|
||||
? t('tours.walk.day.start', { day, time: formatClockTime(option.start, is12h) })
|
||||
@@ -30,12 +30,15 @@ function missingNote(state: WalkScheduleState, choice: WalkDayChoice, stationCou
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The day as a pill: a menu of the walk's days where there is a choice, its name alone where there is none. */
|
||||
/**
|
||||
* The day as a pill: a menu of the walk's days where there is a choice, its name alone where there is none.
|
||||
* It names the day with its date as the menu does, so the opening hours below read for a day that is in view.
|
||||
*/
|
||||
function DayPill({ choice, day }: { choice: WalkDayChoice; day: WalkDayOption }) {
|
||||
const { t, locale } = useTranslation();
|
||||
const is12h = useSettingsStore((state) => state.settings.time_format) === '12h';
|
||||
const menu = useContextMenu();
|
||||
const label = optionLabel(day, t('dayplan.dayN', { n: day.index + 1 }), t, is12h);
|
||||
const label = optionLabel(day, dayMenuLabel(day.day, day.index, t, locale), t, is12h);
|
||||
if (choice.fixed || choice.options.length < 2) {
|
||||
return (
|
||||
<span data-testid="walk-day" className={`${PILL} break-words`} style={fs(11, 'body')}>
|
||||
|
||||
@@ -206,7 +206,7 @@ describe('WalkStationsCard', () => {
|
||||
|
||||
it('on the day it was opened from, shows each arrival, the end of each stay and what the opening hours say', async () => {
|
||||
card(onMonday());
|
||||
expect(screen.getByTestId('walk-day')).toHaveTextContent('Day 1, start 09:00');
|
||||
expect(screen.getByTestId('walk-day')).toHaveTextContent(/^Day 1, .+, start 09:00$/);
|
||||
expect(screen.queryByTestId('walk-day-picker')).not.toBeInTheDocument();
|
||||
const museum = await rowOf('Museum');
|
||||
expect(within(museum).getByText('Arrival about 09:00, stay until 09:30')).toBeInTheDocument();
|
||||
@@ -224,7 +224,7 @@ describe('WalkStationsCard', () => {
|
||||
within(cafe).getByText('Opening hours not checked: a stay before this stop is not entered yet.')
|
||||
).toBeInTheDocument();
|
||||
expect(within(cafe).queryByTestId('walk-station-conflict')).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/Opening hours from OpenStreetMap or your own place entries/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Opening hours from OpenStreetMap or the trip's places/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("names a stop by the trip place it stands for and checks the place's own hours: 17:20 to 18:20 against 18:00", async () => {
|
||||
@@ -259,7 +259,7 @@ describe('WalkStationsCard', () => {
|
||||
seedStore(useSettingsStore, { settings: { time_format: '12h' } });
|
||||
card(onMonday());
|
||||
expect(within(await rowOf('Museum')).getByText('Arrival about 9:00 AM, stay until 9:30 AM')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('walk-day')).toHaveTextContent('Day 1, start 9:00 AM');
|
||||
expect(screen.getByTestId('walk-day')).toHaveTextContent(/^Day 1, .+, start 9:00 AM$/);
|
||||
});
|
||||
|
||||
it('on a day without a start, asks for one and leaves arrivals and hours unsaid', async () => {
|
||||
@@ -267,7 +267,7 @@ describe('WalkStationsCard', () => {
|
||||
expect(
|
||||
await screen.findByText('Give the walk a time on this day to see arrivals and opening hours.')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('walk-day')).toHaveTextContent('Day 1, no start time');
|
||||
expect(screen.getByTestId('walk-day')).toHaveTextContent(/^Day 1, .+, no start time$/);
|
||||
await screen.findAllByTestId('walk-station');
|
||||
expect(screen.queryByText(/Arrival/)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/Opening hours from OpenStreetMap/)).not.toBeInTheDocument();
|
||||
@@ -306,7 +306,7 @@ describe('WalkStationsCard', () => {
|
||||
route: ROUTE,
|
||||
});
|
||||
const picker = screen.getByTestId('walk-day-picker');
|
||||
expect(picker).toHaveTextContent('Day 2, start 10:00');
|
||||
expect(picker).toHaveTextContent(/^Day 2, .+, start 10:00$/);
|
||||
// Tuesday from 10:00: the museum closed at 09:20 already.
|
||||
expect(within(await rowOf('Museum')).getByTestId('walk-station-conflict')).toHaveTextContent(
|
||||
'Museum is closed at 10:00, the stay would last until 10:30; open 09:00 to 09:20'
|
||||
@@ -319,7 +319,7 @@ describe('WalkStationsCard', () => {
|
||||
fireEvent.click(picker);
|
||||
const monday = screen.getByRole('button', { name: /^Day 1, .*no start time$/ });
|
||||
fireEvent.click(monday);
|
||||
expect(screen.getByTestId('walk-day-picker')).toHaveTextContent('Day 1, no start time');
|
||||
expect(screen.getByTestId('walk-day-picker')).toHaveTextContent(/^Day 1, .+, no start time$/);
|
||||
expect(screen.getByText('Give the walk a time on this day to see arrivals and opening hours.')).toBeInTheDocument();
|
||||
expect(screen.queryByText(/Arrival/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -23,6 +23,9 @@ import type { MapsAutocompleteResult } from '@trek/shared';
|
||||
|
||||
type PoiBbox = { south: number; west: number; north: number; east: number };
|
||||
|
||||
/** Every term the index is asked by, whichever kind asks for it. */
|
||||
const EVERY_TERM = Object.values(POI_CATEGORY_TO_TREK).flat();
|
||||
|
||||
/** What one POI request asks the index, and how its answer is labelled. */
|
||||
export interface IndexPoiPlan {
|
||||
wanted: string[];
|
||||
@@ -162,9 +165,7 @@ export function ownedByAsked(
|
||||
row: { category: string | null; categoryPath: string | null },
|
||||
asked: readonly string[],
|
||||
): boolean {
|
||||
// Every term the index is asked by, whichever kind asks for it; read here, not
|
||||
// when the module loads, so a suite that stubs the index client stays loadable.
|
||||
const term = indexRowTerm(Object.values(POI_CATEGORY_TO_TREK).flat(), row.category, row.categoryPath);
|
||||
const term = indexRowTerm(EVERY_TERM, row.category, row.categoryPath);
|
||||
return term === undefined || asked.some((kind) => POI_CATEGORY_TO_TREK[kind]?.includes(term));
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
} from '../../../../src/db/repositories/TourWaypoints.repository';
|
||||
import { createSnapshotTestDb } from '../../../helpers/db-mock';
|
||||
import { createPlace, createTrip, createUser } from '../../../helpers/factories';
|
||||
import { makePlace } from '../../../helpers/factories/places';
|
||||
import { resetTestDb } from '../../../helpers/test-db';
|
||||
import { createTestOrm, type TestOrm } from '../../../helpers/test-orm';
|
||||
import { createTour } from '../../../helpers/tours-repos';
|
||||
@@ -222,9 +223,9 @@ describe('TourWaypointsRepository', () => {
|
||||
|
||||
it('TWREPO-010: listForPlaces reads the waypoints of the tours asked for, by place and sequence, and none for none (CW-02)', async () => {
|
||||
const { trip, place } = tourInTrip();
|
||||
const second = createPlace(testDb, trip.id);
|
||||
const second = await makePlace(t, trip.id);
|
||||
createTour(testDb, second.id);
|
||||
const unasked = createPlace(testDb, trip.id);
|
||||
const unasked = await makePlace(t, trip.id);
|
||||
createTour(testDb, unasked.id);
|
||||
await repo.insertForPlace(second.id, [{ lat: 47.2, lng: 11.2, role: 'start', sequence: 0, stay_minutes: 30 }]);
|
||||
await repo.insertForPlace(place.id, ROUTE);
|
||||
|
||||
@@ -10,7 +10,9 @@ import { Tours } from '../../../../src/db/entities/Tours.entity';
|
||||
import type { ToursRepository } from '../../../../src/db/repositories/Tours.repository';
|
||||
import { createSnapshotTestDb } from '../../../helpers/db-mock';
|
||||
import { createDay, createDayAssignment, createPlace, createTrip, createUser } from '../../../helpers/factories';
|
||||
import { makePlace } from '../../../helpers/factories/places';
|
||||
import { insertRow, updateRows } from '../../../helpers/factories/rows';
|
||||
import { makeDay } from '../../../helpers/factories/trips';
|
||||
import { resetTestDb } from '../../../helpers/test-db';
|
||||
import { createTestOrm, type TestOrm } from '../../../helpers/test-orm';
|
||||
import { createTour } from '../../../helpers/tours-repos';
|
||||
@@ -167,8 +169,8 @@ describe('ToursRepository reads', () => {
|
||||
|
||||
it('TOURREPO-014: the list and the detail say whether the owning place still has no line (CW-11)', async () => {
|
||||
const { trip } = fixture();
|
||||
const pending = createPlace(testDb, trip.id, { name: 'Stations only' });
|
||||
const drawn = createPlace(testDb, trip.id, { name: 'Drawn' });
|
||||
const pending = await makePlace(t, trip.id, { name: 'Stations only' });
|
||||
const drawn = await makePlace(t, trip.id, { name: 'Drawn' });
|
||||
createTour(testDb, pending.id);
|
||||
createTour(testDb, drawn.id);
|
||||
await updateRows(t, Places, { id: drawn.id }, { route_geometry: '[[47,11,500],[47.1,11.1,520]]' });
|
||||
@@ -181,9 +183,9 @@ describe('ToursRepository reads', () => {
|
||||
|
||||
it('TOURREPO-015: dayOfTrip reads a day of the trip with its date, and nothing for a day of another trip', async () => {
|
||||
const { trip, other } = fixture();
|
||||
const dated = createDay(testDb, trip.id, { date: '2026-07-12' });
|
||||
const undated = createDay(testDb, trip.id);
|
||||
const elsewhere = createDay(testDb, other.id, { date: '2026-07-12' });
|
||||
const dated = await makeDay(t, trip.id, { date: '2026-07-12' });
|
||||
const undated = await makeDay(t, trip.id);
|
||||
const elsewhere = await makeDay(t, other.id, { date: '2026-07-12' });
|
||||
|
||||
expect(await repo.dayOfTrip(trip.id, dated.id)).toEqual({ id: dated.id, date: '2026-07-12' });
|
||||
expect(await repo.dayOfTrip(trip.id, undated.id)).toEqual({ id: undated.id, date: null });
|
||||
|
||||
@@ -47,7 +47,8 @@ vi.mock('../../../src/utils/ssrfGuard', () => ({
|
||||
const { mockTrekPlacesById } = vi.hoisted(() => ({
|
||||
mockTrekPlacesById: vi.fn(async (_gers: string): Promise<unknown> => null),
|
||||
}));
|
||||
vi.mock('../../../src/nest/maps/trek-places.client', () => ({
|
||||
vi.mock('../../../src/nest/maps/trek-places.client', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('../../../src/nest/maps/trek-places.client')>()),
|
||||
trekPlacesById: mockTrekPlacesById,
|
||||
}));
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ describe('TourLibraryMcp', () => {
|
||||
importance: 'must',
|
||||
protected: 1,
|
||||
opening_hours: 'Mo-Su 09:00-18:00',
|
||||
linkedPlace: createPlace(db, alps.id, { name: 'Museum' }).id,
|
||||
linkedPlace: (await makePlace(orm, alps.id, { name: 'Museum' })).id,
|
||||
},
|
||||
{ lat: 48.11, lng: 11.51, station_key: 'stop-park', importance: 'optional' },
|
||||
]);
|
||||
|
||||
@@ -572,7 +572,7 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.hours.closes': '{name} يغلق في {closes}، أثناء البقاء من {arrival} إلى {leave} (مفتوح {window})',
|
||||
'tours.walk.hours.unread': 'لم تُقرأ ساعات العمل هنا: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'ساعات العمل من OpenStreetMap أو من أماكنك التي أدخلتها بنفسك، مقارنةً بوقت الوصول المقدّر بالتوقيت المحلي للخطة؛ تحقق منها قبل الذهاب. المحطة بلا ساعات عمل غير معروفة، وليست مفتوحة.',
|
||||
'ساعات العمل من OpenStreetMap أو من أماكن الرحلة، مقارنةً بوقت الوصول المقدّر بالتوقيت المحلي للخطة؛ تحقق منها قبل الذهاب. المحطة بلا ساعات عمل غير معروفة، وليست مفتوحة.',
|
||||
'tours.walk.hours.noStart': 'حدد وقتًا للجولة في هذا اليوم لرؤية أوقات الوصول وساعات العمل.',
|
||||
'tours.walk.day.label': 'الأوقات حسب',
|
||||
'tours.walk.day.start': '{day}، البدء {time}',
|
||||
|
||||
@@ -571,7 +571,7 @@ const tours: TranslationStrings = {
|
||||
'{name} {closes} vaxtı bağlanır, {arrival} ilə {leave} arası qalma zamanı (açıqdır {window})',
|
||||
'tours.walk.hours.unread': 'Açılış saatları burada oxunmadı: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Açılış saatları OpenStreetMap-dən və ya öz yerlərindən götürülür, planın yerli vaxtı ilə təxmini çatma vaxtına qarşı müqayisə olunur; getməzdən əvvəl yoxla. Saatları olmayan dayanacaq naməlumdur, açıq deyil.',
|
||||
'Açılış saatları OpenStreetMap-dən və ya səyahətin yerlərindən götürülür, planın yerli vaxtı ilə təxmini çatma vaxtına qarşı müqayisə olunur; getməzdən əvvəl yoxla. Saatları olmayan dayanacaq naməlumdur, 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.walk.day.label': 'Vaxtlar üçün gün',
|
||||
'tours.walk.day.start': '{day}, başlanğıc {time}',
|
||||
|
||||
@@ -576,7 +576,7 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.hours.closes': '{name} fecha às {closes}, durante a permanência das {arrival} às {leave} (abre {window})',
|
||||
'tours.walk.hours.unread': 'Horário não lido aqui: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Horários do OpenStreetMap ou dos seus próprios lugares, comparados com a chegada estimada no horário local do plano; confira antes de ir. Uma parada sem horário é desconhecida, não aberta.',
|
||||
'Horários do OpenStreetMap ou dos lugares da viagem, comparados com a chegada estimada no horário local do plano; 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.walk.day.label': 'Horários para',
|
||||
'tours.walk.day.start': '{day}, saída às {time}',
|
||||
|
||||
@@ -577,7 +577,7 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.hours.closes': "{name} tanca a les {closes}, durant l'estada de {arrival} a {leave} (obre {window})",
|
||||
'tours.walk.hours.unread': 'Horari no llegit aquí: {text}',
|
||||
'tours.walk.hours.source':
|
||||
"Horaris d'OpenStreetMap o dels teus propis llocs, comparats amb l'arribada estimada en l'hora local del pla; comprova'ls abans d'anar-hi. Una parada sense horari és desconeguda, no oberta.",
|
||||
"Horaris d'OpenStreetMap o dels llocs del viatge, comparats amb l'arribada estimada en l'hora local del pla; comprova'ls 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.walk.day.label': 'Horaris per a',
|
||||
'tours.walk.day.start': '{day}, sortida a les {time}',
|
||||
|
||||
@@ -566,7 +566,7 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.hours.closes': '{name}: zavírá v {closes}, během pobytu od {arrival} do {leave} (otevřeno {window})',
|
||||
'tours.walk.hours.unread': 'Otevírací dobu zde nelze vyhodnotit: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Otevírací doby z OpenStreetMap nebo z tvých vlastních míst, porovnané s odhadovaným příchodem v místním čase plánu; před cestou si je ověř. Zastávka bez časů je neznámá, ne otevřená.',
|
||||
'Otevírací doby z OpenStreetMap nebo z míst výletu, porovnané s odhadovaným příchodem v místním čase plánu; před cestou si je 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.walk.day.label': 'Časy pro',
|
||||
'tours.walk.day.start': '{day}, začátek {time}',
|
||||
|
||||
@@ -582,7 +582,7 @@ const tours: TranslationStrings = {
|
||||
'{name} schließt um {closes}, während des Aufenthalts von {arrival} bis {leave} (geöffnet {window})',
|
||||
'tours.walk.hours.unread': 'Öffnungszeiten hier nicht auswertbar: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Öffnungszeiten aus OpenStreetMap oder aus deinen eigenen Ortsangaben, verglichen mit der geschätzten Ankunft in der Ortszeit des Plans; vor Ort lieber prüfen. Eine Station ohne Zeiten ist unbekannt, nicht geöffnet.',
|
||||
'Öffnungszeiten aus OpenStreetMap oder den Orten der Reise, verglichen mit der geschätzten Ankunft in der Ortszeit des Plans; 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.walk.day.label': 'Zeiten für',
|
||||
'tours.walk.day.start': '{day}, Beginn {time}',
|
||||
|
||||
@@ -567,7 +567,7 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.hours.closes': '{name} closes at {closes}, during the stay from {arrival} to {leave} (open {window})',
|
||||
'tours.walk.hours.unread': 'Opening hours not read here: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Opening hours from OpenStreetMap or your own place entries, compared with the estimated arrival in the local time of the plan; check before you go. A stop without hours is unknown, not open.',
|
||||
"Opening hours from OpenStreetMap or the trip's places, compared with the estimated arrival in the local time of the plan; 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.walk.day.label': 'Times for',
|
||||
'tours.walk.day.start': '{day}, start {time}',
|
||||
|
||||
@@ -576,7 +576,7 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.hours.closes': '{name} cierra a las {closes}, durante la estancia de {arrival} a {leave} (abre {window})',
|
||||
'tours.walk.hours.unread': 'Horario no interpretado aquí: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Horarios de OpenStreetMap o de tus propios lugares, comparados con la llegada estimada en la hora local del plan; compruébalos antes de ir. Una parada sin horario es desconocida, no abierta.',
|
||||
'Horarios de OpenStreetMap o de los lugares del viaje, comparados con la llegada estimada en la hora local del plan; compruébalos 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.walk.day.label': 'Horarios para',
|
||||
'tours.walk.day.start': '{day}, salida a las {time}',
|
||||
|
||||
@@ -568,7 +568,7 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.hours.closes': '{name} suletakse kell {closes}, viibimise ajal {arrival} kuni {leave} (avatud {window})',
|
||||
'tours.walk.hours.unread': 'Lahtiolekuaegu siin ei loetud: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Lahtiolekuajad OpenStreetMapist või sinu enda kohtadest, võrreldud hinnangulise saabumisega plaani kohaliku aja järgi; kontrolli enne minekut. Ajata peatus on teadmata, mitte avatud.',
|
||||
'Lahtiolekuajad OpenStreetMapist või reisi kohtadest, võrreldud hinnangulise saabumisega plaani kohaliku aja järgi; kontrolli enne minekut. Ajata peatus on teadmata, mitte avatud.',
|
||||
'tours.walk.hours.noStart': 'Anna jalutusele sel päeval kellaaeg, et näha saabumisi ja lahtiolekuaegu.',
|
||||
'tours.walk.day.label': 'Ajad päeval',
|
||||
'tours.walk.day.start': '{day}, algus {time}',
|
||||
|
||||
@@ -584,7 +584,7 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.hours.closes': "{name} ferme à {closes}, pendant l'arrêt de {arrival} à {leave} (ouvert {window})",
|
||||
'tours.walk.hours.unread': 'Horaires non lus ici : {text}',
|
||||
'tours.walk.hours.source':
|
||||
"Horaires d'OpenStreetMap ou de tes propres lieux, comparés à l'arrivée estimée à l'heure locale du plan ; vérifie avant d'y aller. Une étape sans horaires est inconnue, pas ouverte.",
|
||||
"Horaires d'OpenStreetMap ou des lieux du voyage, comparés à l'arrivée estimée à l'heure locale du plan ; 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.walk.day.label': 'Horaires pour',
|
||||
'tours.walk.day.start': '{day}, départ à {time}',
|
||||
|
||||
@@ -587,7 +587,7 @@ const tours: TranslationStrings = {
|
||||
'{name}: κλείνει στις {closes}, κατά την παραμονή από {arrival} έως {leave} (ανοιχτό {window})',
|
||||
'tours.walk.hours.unread': 'Το ωράριο δεν διαβάστηκε εδώ: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Ωράρια από το OpenStreetMap ή από τα δικά σου μέρη, σε σύγκριση με την εκτιμώμενη άφιξη στην τοπική ώρα του πλάνου. Έλεγξε πριν πας. Μια στάση χωρίς ωράριο είναι άγνωστη, όχι ανοιχτή.',
|
||||
'Ωράρια από το OpenStreetMap ή από τα μέρη του ταξιδιού, σε σύγκριση με την εκτιμώμενη άφιξη στην τοπική ώρα του πλάνου. Έλεγξε πριν πας. Μια στάση χωρίς ωράριο είναι άγνωστη, όχι ανοιχτή.',
|
||||
'tours.walk.hours.noStart': 'Δώσε ώρα στη βόλτα αυτή τη μέρα για να δεις αφίξεις και ωράρια.',
|
||||
'tours.walk.day.label': 'Ώρες για',
|
||||
'tours.walk.day.start': '{day}, έναρξη {time}',
|
||||
|
||||
@@ -577,7 +577,7 @@ const tours: TranslationStrings = {
|
||||
'{name} {closes} órakor zár, a {arrival} és {leave} közötti tartózkodás alatt (nyitva {window})',
|
||||
'tours.walk.hours.unread': 'A nyitvatartás itt nem értelmezhető: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Nyitvatartás az OpenStreetMapből vagy a saját helyeidből, a becsült érkezéshez mérve a terv helyi ideje szerint; indulás előtt ellenőrizd. Az időpontok nélküli megálló ismeretlen, nem nyitva.',
|
||||
'Nyitvatartás az OpenStreetMapből vagy az utazás helyeiből, a becsült érkezéshez mérve a terv helyi ideje szerint; indulás előtt ellenőrizd. Az időpontok nélküli megálló ismeretlen, nem nyitva.',
|
||||
'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.walk.day.label': 'Időpontok:',
|
||||
'tours.walk.day.start': '{day}, kezdés {time}',
|
||||
|
||||
@@ -571,7 +571,7 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.hours.closes': '{name} tutup pukul {closes}, saat singgah dari {arrival} sampai {leave} (buka {window})',
|
||||
'tours.walk.hours.unread': 'Jam buka tidak terbaca di sini: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Jam buka dari OpenStreetMap atau dari tempat yang kamu isi sendiri, dibandingkan dengan perkiraan waktu tiba menurut waktu setempat rencana; periksa sebelum berangkat. Titik tanpa jam buka berarti tidak diketahui, bukan buka.',
|
||||
'Jam buka dari OpenStreetMap atau dari tempat-tempat perjalanan, dibandingkan dengan perkiraan waktu tiba menurut waktu setempat rencana; periksa sebelum berangkat. Titik tanpa jam buka berarti tidak diketahui, bukan buka.',
|
||||
'tours.walk.hours.noStart': 'Beri rute waktu mulai di hari ini untuk melihat waktu tiba dan jam buka.',
|
||||
'tours.walk.day.label': 'Waktu untuk',
|
||||
'tours.walk.day.start': '{day}, mulai {time}',
|
||||
|
||||
@@ -578,7 +578,7 @@ const tours: TranslationStrings = {
|
||||
'{name} chiude alle {closes}, durante la sosta dalle {arrival} alle {leave} (aperto {window})',
|
||||
'tours.walk.hours.unread': 'Orari non interpretati qui: {text}',
|
||||
'tours.walk.hours.source':
|
||||
"Orari da OpenStreetMap o dai tuoi luoghi, confrontati con l'arrivo stimato nell'ora locale del piano; verifica prima di andare. Una tappa senza orari è sconosciuta, non aperta.",
|
||||
"Orari da OpenStreetMap o dai luoghi del viaggio, confrontati con l'arrivo stimato nell'ora locale del piano; 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.walk.day.label': 'Orari per',
|
||||
'tours.walk.day.start': '{day}, partenza alle {time}',
|
||||
|
||||
@@ -558,7 +558,7 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.hours.closes': '{name} は {closes} に閉まります。{arrival} から {leave} の滞在中です(営業 {window})',
|
||||
'tours.walk.hours.unread': '営業時間はここでは読み取れません:{text}',
|
||||
'tours.walk.hours.source':
|
||||
'OpenStreetMap または自分で入力した場所の営業時間を、計画の現地時刻での推定到着時刻と照合しています。出かける前に確認してください。営業時間のない立ち寄り先は「不明」で、営業中とはみなしません。',
|
||||
'OpenStreetMap または旅行の場所の営業時間を、計画の現地時刻での推定到着時刻と照合しています。出かける前に確認してください。営業時間のない立ち寄り先は「不明」で、営業中とはみなしません。',
|
||||
'tours.walk.hours.noStart': 'この日の開始時刻を設定すると、到着時刻と営業時間が表示されます。',
|
||||
'tours.walk.day.label': '時刻の基準日',
|
||||
'tours.walk.day.start': '{day}、開始 {time}',
|
||||
|
||||
@@ -555,7 +555,7 @@ const tours: TranslationStrings = {
|
||||
'{name}은(는) {closes}에 문을 닫습니다. {arrival}부터 {leave}까지 머무는 중입니다(영업 {window})',
|
||||
'tours.walk.hours.unread': '여기서는 영업시간을 읽지 못했습니다: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'OpenStreetMap 또는 직접 입력한 장소의 영업시간을 계획 현지 시간 기준의 예상 도착 시간과 비교합니다. 가기 전에 확인하세요. 영업시간이 없는 정류지는 영업 중이 아니라 미상입니다.',
|
||||
'OpenStreetMap 또는 여행 장소의 영업시간을 계획 현지 시간 기준의 예상 도착 시간과 비교합니다. 가기 전에 확인하세요. 영업시간이 없는 정류지는 영업 중이 아니라 미상입니다.',
|
||||
'tours.walk.hours.noStart': '이날 산책에 시간을 지정하면 도착 시간과 영업시간을 볼 수 있습니다.',
|
||||
'tours.walk.day.label': '시간 기준',
|
||||
'tours.walk.day.start': '{day}, 시작 {time}',
|
||||
|
||||
@@ -579,7 +579,7 @@ const tours: TranslationStrings = {
|
||||
'{name} sluit om {closes}, tijdens het verblijf van {arrival} tot {leave} (geopend {window})',
|
||||
'tours.walk.hours.unread': 'Openingstijden hier niet gelezen: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Openingstijden uit OpenStreetMap of je eigen plekken, vergeleken met de geschatte aankomst in de lokale tijd van het plan; controleer ze voor je gaat. Een halte zonder tijden is onbekend, niet open.',
|
||||
'Openingstijden uit OpenStreetMap of de plekken van de reis, vergeleken met de geschatte aankomst in de lokale tijd van het plan; 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.walk.day.label': 'Tijden voor',
|
||||
'tours.walk.day.start': '{day}, vertrek {time}',
|
||||
|
||||
@@ -586,7 +586,7 @@ const tours: TranslationStrings = {
|
||||
'{name}: zamknięcie o {closes}, w trakcie pobytu od {arrival} do {leave} (otwarte {window})',
|
||||
'tours.walk.hours.unread': 'Godziny otwarcia nieodczytane: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Godziny otwarcia z OpenStreetMap lub z twoich własnych miejsc, porównane z szacowanym przyjściem w czasie lokalnym planu; sprawdź przed wyjściem. Przystanek bez godzin jest nieznany, nie otwarty.',
|
||||
'Godziny otwarcia z OpenStreetMap lub z miejsc podróży, porównane z szacowanym przyjściem w czasie lokalnym planu; 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.walk.day.label': 'Godziny dla',
|
||||
'tours.walk.day.start': '{day}, początek {time}',
|
||||
|
||||
@@ -588,7 +588,7 @@ const tours: TranslationStrings = {
|
||||
'{name}: закрывается в {closes}, во время пребывания с {arrival} до {leave} (открыто {window})',
|
||||
'tours.walk.hours.unread': 'Часы работы здесь не разобраны: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Часы работы из OpenStreetMap или из твоих собственных мест, относительно расчётного прибытия по местному времени плана; проверь перед выходом. Остановка без часов неизвестна, а не открыта.',
|
||||
'Часы работы из OpenStreetMap или из мест поездки, относительно расчётного прибытия по местному времени плана; проверь перед выходом. Остановка без часов неизвестна, а не открыта.',
|
||||
'tours.walk.hours.noStart': 'Задай прогулке время в этот день, чтобы видеть прибытия и часы работы.',
|
||||
'tours.walk.day.label': 'Время для',
|
||||
'tours.walk.day.start': '{day}, начало в {time}',
|
||||
|
||||
@@ -568,7 +568,7 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.hours.closes': '{name}: zatvára o {closes}, počas pobytu od {arrival} do {leave} (otvorené {window})',
|
||||
'tours.walk.hours.unread': 'Otváracie hodiny tu nemožno vyhodnotiť: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Otváracie hodiny z OpenStreetMap alebo z tvojich vlastných miest, porovnané s odhadovaným príchodom v miestnom čase plánu; pred cestou si ich over. Zastávka bez časov je neznáma, nie otvorená.',
|
||||
'Otváracie hodiny z OpenStreetMap alebo z miest výletu, porovnané s odhadovaným príchodom v miestnom čase plánu; 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.walk.day.label': 'Časy pre',
|
||||
'tours.walk.day.start': '{day}, začiatok {time}',
|
||||
|
||||
@@ -567,7 +567,7 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.hours.closes': '{name} stänger kl. {closes}, under vistelsen {arrival} till {leave} (öppet {window})',
|
||||
'tours.walk.hours.unread': 'Öppettiderna kunde inte läsas här: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Öppettider från OpenStreetMap eller dina egna platser, jämförda med beräknad ankomst i planens lokala tid; kontrollera innan du går. Ett stopp utan tider är okänt, inte öppet.',
|
||||
'Öppettider från OpenStreetMap eller resans platser, jämförda med beräknad ankomst i planens lokala tid; 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.walk.day.label': 'Tider för',
|
||||
'tours.walk.day.start': '{day}, start kl. {time}',
|
||||
|
||||
@@ -542,7 +542,7 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.hours.closes': '{name} ปิด {closes} ระหว่างแวะตั้งแต่ {arrival} ถึง {leave} (เปิด {window})',
|
||||
'tours.walk.hours.unread': 'อ่านเวลาทำการไม่ได้: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'เวลาทำการจาก OpenStreetMap หรือจากสถานที่ที่คุณใส่เอง เทียบกับเวลาถึงโดยประมาณตามเวลาท้องถิ่นของแผน โปรดตรวจสอบก่อนไป จุดแวะที่ไม่มีเวลาทำการถือว่าไม่ทราบ ไม่ใช่เปิดอยู่',
|
||||
'เวลาทำการจาก OpenStreetMap หรือจากสถานที่ในทริป เทียบกับเวลาถึงโดยประมาณตามเวลาท้องถิ่นของแผน โปรดตรวจสอบก่อนไป จุดแวะที่ไม่มีเวลาทำการถือว่าไม่ทราบ ไม่ใช่เปิดอยู่',
|
||||
'tours.walk.hours.noStart': 'กำหนดเวลาให้ทริปในวันนี้เพื่อดูเวลาถึงและเวลาทำการ',
|
||||
'tours.walk.day.label': 'เวลาสำหรับ',
|
||||
'tours.walk.day.start': '{day} เริ่ม {time}',
|
||||
|
||||
@@ -572,7 +572,7 @@ const tours: TranslationStrings = {
|
||||
'{name} {closes} saatinde kapanıyor, {arrival} ile {leave} arasındaki kalış sırasında (açık {window})',
|
||||
'tours.walk.hours.unread': 'Açılış saatleri burada okunamadı: {text}',
|
||||
'tours.walk.hours.source':
|
||||
"Açılış saatleri OpenStreetMap'ten veya kendi yerlerinden, planın yerel saatine göre tahmini varışla karşılaştırılır; gitmeden önce kontrol et. Saati olmayan durak bilinmiyordur, açık değildir.",
|
||||
"Açılış saatleri OpenStreetMap'ten veya seyahatin yerlerinden, planın yerel saatine göre tahmini varışla karşılaştırılır; 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.walk.day.label': 'Saatler:',
|
||||
'tours.walk.day.start': '{day}, başlangıç {time}',
|
||||
|
||||
@@ -584,7 +584,7 @@ const tours: TranslationStrings = {
|
||||
'{name}: зачиняється о {closes}, під час перебування з {arrival} до {leave} (відчинено {window})',
|
||||
'tours.walk.hours.unread': 'Години роботи тут не розібрано: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Години роботи з OpenStreetMap або з твоїх власних місць, відносно розрахункового прибуття за місцевим часом плану; перевір перед виходом. Зупинка без годин невідома, а не відкрита.',
|
||||
'Години роботи з OpenStreetMap або з місць подорожі, відносно розрахункового прибуття за місцевим часом плану; перевір перед виходом. Зупинка без годин невідома, а не відкрита.',
|
||||
'tours.walk.hours.noStart': 'Задай прогулянці час цього дня, щоб бачити прибуття й години роботи.',
|
||||
'tours.walk.day.label': 'Час для',
|
||||
'tours.walk.day.start': '{day}, початок о {time}',
|
||||
|
||||
@@ -566,7 +566,7 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.hours.closes': '{name} đóng cửa lúc {closes}, trong lúc ở từ {arrival} đến {leave} (mở {window})',
|
||||
'tours.walk.hours.unread': 'Không đọc được giờ mở cửa ở đây: {text}',
|
||||
'tours.walk.hours.source':
|
||||
'Giờ mở cửa từ OpenStreetMap hoặc từ các địa điểm bạn tự nhập, so với giờ đến ước tính theo giờ địa phương của kế hoạch; 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ở.',
|
||||
'Giờ mở cửa từ OpenStreetMap hoặc từ các địa điểm của chuyến đi, so với giờ đến ước tính theo giờ địa phương của kế hoạch; 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.walk.day.label': 'Thời gian theo',
|
||||
'tours.walk.day.start': '{day}, bắt đầu {time}',
|
||||
|
||||
@@ -522,7 +522,7 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.hours.closes': '{name} 在 {closes} 關門,正值 {arrival} 至 {leave} 的停留期間(營業時間 {window})',
|
||||
'tours.walk.hours.unread': '此處無法解析營業時間:{text}',
|
||||
'tours.walk.hours.source':
|
||||
'營業時間來自 OpenStreetMap 或你自己填寫的地點,並按計畫的當地時間與預計抵達時間對照;出發前請再確認。沒有營業時間的站點視為未知,而非營業中。',
|
||||
'營業時間來自 OpenStreetMap 或行程中的地點,並按計畫的當地時間與預計抵達時間對照;出發前請再確認。沒有營業時間的站點視為未知,而非營業中。',
|
||||
'tours.walk.hours.noStart': '為當天的漫步設定時間,即可查看抵達時間和營業時間。',
|
||||
'tours.walk.day.label': '時間依據',
|
||||
'tours.walk.day.start': '{day},{time} 出發',
|
||||
|
||||
@@ -523,7 +523,7 @@ const tours: TranslationStrings = {
|
||||
'tours.walk.hours.closes': '{name} 在 {closes} 关门,正值 {arrival} 至 {leave} 的停留期间(营业时间 {window})',
|
||||
'tours.walk.hours.unread': '此处无法解析营业时间:{text}',
|
||||
'tours.walk.hours.source':
|
||||
'营业时间来自 OpenStreetMap 或你自己填写的地点,并按计划的当地时间与预计到达时间对照;出发前请再确认。没有营业时间的站点视为未知,而非营业中。',
|
||||
'营业时间来自 OpenStreetMap 或行程中的地点,并按计划的当地时间与预计到达时间对照;出发前请再确认。没有营业时间的站点视为未知,而非营业中。',
|
||||
'tours.walk.hours.noStart': '为当天的漫步设定时间,即可查看到达时间和营业时间。',
|
||||
'tours.walk.day.label': '时间依据',
|
||||
'tours.walk.day.start': '{day},{time} 出发',
|
||||
|
||||
@@ -21,7 +21,11 @@ const SATURDAY = '2026-10-24';
|
||||
const SUNDAY = '2026-10-25';
|
||||
|
||||
const at = (clock: string, atLeast = false) => ({ arrival: minutesOf(clock) ?? Number.NaN, atLeast });
|
||||
const osm = (windows: Array<[number, number]>): StationHours => ({ source: 'osm', windows });
|
||||
const osm = (windows: Array<[number, number]>, continues = false): StationHours => ({
|
||||
source: 'osm',
|
||||
windows,
|
||||
continues,
|
||||
});
|
||||
|
||||
describe('minutesOf and clockOf', () => {
|
||||
it('reads a time as TREK stores it, with or without seconds or a leading zero', () => {
|
||||
@@ -92,6 +96,8 @@ describe('openWindows (OpenStreetMap)', () => {
|
||||
expect(openWindows(text, MONDAY)).toBeNull();
|
||||
}
|
||||
expect(openWindows(week, '19.10.2026')).toBeNull();
|
||||
expect(openWindows(week, '2026-02-31')).toBeNull();
|
||||
expect(openWindows(week, '2026-13-01')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,6 +154,7 @@ describe('stationHoursOn', () => {
|
||||
expect(stationHoursOn({ place, osm: 'Mo-Fr 09:00-18:00' }, MONDAY)).toEqual({
|
||||
source: 'place',
|
||||
windows: [[660, 1020]],
|
||||
continues: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -155,13 +162,42 @@ describe('stationHoursOn', () => {
|
||||
expect(stationHoursOn({ place, osm: 'Mo-Fr 09:00-18:00' }, TUESDAY)).toEqual({
|
||||
source: 'osm',
|
||||
windows: [[540, 1080]],
|
||||
continues: false,
|
||||
});
|
||||
expect(stationHoursOn({ osm: 'sunrise-sunset' }, TUESDAY)).toEqual({ source: 'osm', unread: 'sunrise-sunset' });
|
||||
});
|
||||
|
||||
it('is null where neither says anything', () => {
|
||||
it('is null where neither says anything, or for a date it cannot read', () => {
|
||||
expect(stationHoursOn({ place, osm: ' ' }, TUESDAY)).toBeNull();
|
||||
expect(stationHoursOn({}, MONDAY)).toBeNull();
|
||||
expect(stationHoursOn({ place, osm: 'Mo-Fr 09:00-18:00' }, '2026-02-31')).toBeNull();
|
||||
});
|
||||
|
||||
it('runs an OpenStreetMap last window on past midnight only where the next date opens at midnight', () => {
|
||||
const continuesOn = (text: string, date: string) => {
|
||||
const hours = stationHoursOn({ osm: text }, date);
|
||||
return hours && 'continues' in hours ? hours.continues : null;
|
||||
};
|
||||
expect(continuesOn('24/7', SUNDAY)).toBe(true);
|
||||
expect(continuesOn('Mo 18:00-24:00; Tu 00:00-02:00', MONDAY)).toBe(true);
|
||||
expect(continuesOn('Mo-Su 10:00-24:00', MONDAY)).toBe(false);
|
||||
expect(continuesOn('Mo-Su 10:00-18:00', MONDAY)).toBe(false);
|
||||
expect(continuesOn('Mo 18:00-24:00; Tu off', MONDAY)).toBe(false);
|
||||
});
|
||||
|
||||
it("runs a trip place's last window on past midnight only where the next date opens at midnight", () => {
|
||||
const week = (monday: object, tuesday: object) =>
|
||||
JSON.stringify([monday, tuesday, ...Array.from({ length: 5 }, () => ({ closed: true }))]);
|
||||
const continuesOn = (raw: string) => {
|
||||
const hours = stationHoursOn({ place: raw }, MONDAY);
|
||||
return hours && 'continues' in hours ? hours.continues : null;
|
||||
};
|
||||
expect(continuesOn(week({ closed: false, open: '18:00', close: '02:00' }, { closed: true }))).toBe(true);
|
||||
const allDay = { closed: false, open: '00:00', close: '00:00' };
|
||||
expect(continuesOn(week(allDay, allDay))).toBe(true);
|
||||
expect(continuesOn(week({ closed: false, open: '18:00', close: '00:00' }, { closed: true }))).toBe(false);
|
||||
expect(continuesOn(week({ closed: false, open: '18:00', close: '00:00' }, { closed: false }))).toBe(false);
|
||||
expect(continuesOn(week({ closed: true }, allDay))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -181,14 +217,27 @@ describe('stationFit', () => {
|
||||
it('is closed at an arrival outside the windows, with the windows of the day', () => {
|
||||
expect(stationFit(museum, at('09:00'), 30)).toEqual({ kind: 'closed', source: 'osm', windows: [[600, 1080]] });
|
||||
expect(stationFit(museum, at('18:00'), null)).toEqual({ kind: 'closed', source: 'osm', windows: [[600, 1080]] });
|
||||
expect(stationFit({ source: 'place', windows: [] }, at('12:00'), 30)).toEqual({
|
||||
expect(stationFit({ source: 'place', windows: [], continues: false }, at('12:00'), 30)).toEqual({
|
||||
kind: 'closedAllDay',
|
||||
source: 'place',
|
||||
});
|
||||
});
|
||||
|
||||
it('takes a window running to midnight as open past it', () => {
|
||||
expect(stationFit(osm([[600, 1440]]), at('23:20'), 90).kind).toBe('open');
|
||||
it('takes a window to midnight as open past it only where the hours run on into the next date', () => {
|
||||
expect(stationFit(osm([[600, 1440]], true), at('23:20'), 90).kind).toBe('open');
|
||||
expect(stationFit(osm([[600, 1440]]), at('23:30'), 60)).toEqual({
|
||||
kind: 'closes',
|
||||
source: 'osm',
|
||||
window: [600, 1440],
|
||||
});
|
||||
expect(stationFit(osm([[600, 1440]]), at('23:20'), 30).kind).toBe('open');
|
||||
expect(stationFit(osm([[600, 1440]]), at('23:30', true), 60).kind).toBe('closes');
|
||||
});
|
||||
|
||||
it('names the closing at midnight of OpenStreetMap hours ending at 24:00 for a stay past it', () => {
|
||||
const hours = stationHoursOn({ osm: 'Mo-Su 10:00-24:00' }, MONDAY);
|
||||
expect(stationFit(hours, at('23:30'), 60)).toEqual({ kind: 'closes', source: 'osm', window: [600, 1440] });
|
||||
expect(stationFit(stationHoursOn({ osm: '24/7' }, MONDAY), at('23:30'), 60).kind).toBe('open');
|
||||
});
|
||||
|
||||
it('says nothing for hours not known, and gives unread hours back as written', () => {
|
||||
|
||||
@@ -28,8 +28,14 @@ export type HoursWindow = readonly [number, number];
|
||||
/** Where a stop's hours come from: OpenStreetMap, or the trip place's own entry. */
|
||||
export type HoursSource = 'osm' | 'place';
|
||||
|
||||
/** A stop's hours on one date: its open windows (none when closed all day), or text that could not be read. */
|
||||
export type StationHours = { source: HoursSource; windows: readonly HoursWindow[] } | { source: 'osm'; unread: string };
|
||||
/**
|
||||
* A stop's hours on one date: its open windows (none when closed all day), or
|
||||
* text that could not be read. `continues` says the last window, ending at
|
||||
* midnight, runs on into the next date, as it does around the clock or for a
|
||||
* night past midnight; a place closing at midnight does not.
|
||||
*/
|
||||
export type StationHours =
|
||||
{ source: HoursSource; windows: readonly HoursWindow[]; continues: boolean } | { source: 'osm'; unread: string };
|
||||
|
||||
function clockMinutes(hours: number, minutes: number): number | null {
|
||||
return hours < 24 && minutes < 60 ? hours * 60 + minutes : null;
|
||||
@@ -58,8 +64,12 @@ export function clockOf(minutes: number): string {
|
||||
function weekdayOf(date: string): number | null {
|
||||
const match = ISO_DATE.exec(date);
|
||||
if (!match) return null;
|
||||
const day = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))).getUTCDay();
|
||||
return Number.isNaN(day) ? null : (day + 6) % 7;
|
||||
const month = Number(match[2]) - 1;
|
||||
const dayOfMonth = Number(match[3]);
|
||||
const day = new Date(Date.UTC(Number(match[1]), month, dayOfMonth));
|
||||
// A date that does not exist, such as 2026-02-31, would roll over into the next month.
|
||||
if (day.getUTCMonth() !== month || day.getUTCDate() !== dayOfMonth) return null;
|
||||
return (day.getUTCDay() + 6) % 7;
|
||||
}
|
||||
|
||||
/** The weekdays of an OpenStreetMap day list such as `Mo-Fr,Su`; a range may wrap round the week (`Sa-Mo`). */
|
||||
@@ -106,8 +116,11 @@ function ruleOf(rule: string): { days: Set<number>; windows: HoursWindow[] } | n
|
||||
/** The windows OpenStreetMap's hours give a date, in minutes; null where they cannot be read. */
|
||||
export function openWindows(hours: string, date: string): HoursWindow[] | null {
|
||||
const weekday = weekdayOf(date);
|
||||
if (weekday === null) return null;
|
||||
const text = hours.trim();
|
||||
return weekday === null ? null : osmWindows(hours.trim(), weekday);
|
||||
}
|
||||
|
||||
/** The windows OpenStreetMap's hours give a weekday, Monday 0; null where they cannot be read. */
|
||||
function osmWindows(text: string, weekday: number): HoursWindow[] | null {
|
||||
if (text === '24/7') return [[0, DAY_MINUTES]];
|
||||
let windows: HoursWindow[] = [];
|
||||
for (const raw of text.split(';')) {
|
||||
@@ -147,7 +160,11 @@ function spillOf(day: PlaceHoursDay | undefined): HoursWindow | null {
|
||||
export function placeWindows(raw: string | null | undefined, date: string): HoursWindow[] | null {
|
||||
const week = parsePlaceHours(raw);
|
||||
const weekday = weekdayOf(date);
|
||||
if (!week || weekday === null) return null;
|
||||
return week && weekday !== null ? placeWeekWindows(week, weekday) : null;
|
||||
}
|
||||
|
||||
/** The windows of one weekday of a place's own week, Monday 0, with the night of the day before. */
|
||||
function placeWeekWindows(week: readonly PlaceHoursDay[], weekday: number): HoursWindow[] | null {
|
||||
const today = dayWindows(week[weekday]);
|
||||
if (today === null) return null;
|
||||
const spill = spillOf(week[(weekday + 6) % 7]);
|
||||
@@ -157,18 +174,35 @@ export function placeWindows(raw: string | null | undefined, date: string): Hour
|
||||
/**
|
||||
* A stop's hours on a date: the trip place it stands for first, as the
|
||||
* travellers entered them, else what OpenStreetMap had for it; null where
|
||||
* neither says anything.
|
||||
* neither says anything or the date cannot be read.
|
||||
*/
|
||||
export function stationHoursOn(
|
||||
hours: { place?: string | null; osm?: string | null },
|
||||
date: string,
|
||||
): StationHours | null {
|
||||
const own = placeWindows(hours.place, date);
|
||||
if (own) return { source: 'place', windows: own };
|
||||
const weekday = weekdayOf(date);
|
||||
if (weekday === null) return null;
|
||||
const tomorrow = (weekday + 1) % 7;
|
||||
const week = parsePlaceHours(hours.place);
|
||||
const own = week && placeWeekWindows(week, weekday);
|
||||
if (week && own) {
|
||||
return { source: 'place', windows: own, continues: runsOn(own, placeWeekWindows(week, tomorrow)) };
|
||||
}
|
||||
const text = hours.osm?.trim();
|
||||
if (!text) return null;
|
||||
const windows = openWindows(text, date);
|
||||
return windows ? { source: 'osm', windows } : { source: 'osm', unread: text };
|
||||
const windows = osmWindows(text, weekday);
|
||||
return windows
|
||||
? { source: 'osm', windows, continues: runsOn(windows, osmWindows(text, tomorrow)) }
|
||||
: { source: 'osm', unread: text };
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a window of the day ends at midnight and the next day opens at
|
||||
* midnight, so the hours run on past it. Every window is looked at, as the
|
||||
* hours text keeps the order it was written in.
|
||||
*/
|
||||
function runsOn(today: readonly HoursWindow[], tomorrow: readonly HoursWindow[] | null): boolean {
|
||||
return today.some(([, to]) => to === DAY_MINUTES) && (tomorrow?.some(([from]) => from === 0) ?? false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,10 +236,10 @@ function fitInWindow(
|
||||
source: HoursSource,
|
||||
window: HoursWindow,
|
||||
leave: number | null,
|
||||
later: { atLeast: boolean; windowAfter: boolean },
|
||||
later: { atLeast: boolean; windowAfter: boolean; continues: boolean },
|
||||
): StationFit {
|
||||
// A window running to midnight is taken as open past it.
|
||||
const closes = leave !== null && leave > window[1] && window[1] !== DAY_MINUTES;
|
||||
// A window to midnight is open past it only where the hours run on into the next date.
|
||||
const closes = leave !== null && leave > window[1] && !(window[1] === DAY_MINUTES && later.continues);
|
||||
// An arrival that may come later can still miss the closing, or reach the next window instead.
|
||||
if (later.atLeast)
|
||||
return closes && !later.windowAfter ? { kind: 'closes', source, window } : { kind: 'uncertain', source };
|
||||
@@ -221,12 +255,12 @@ export function stationFit(hours: StationHours | null, at: WalkArrival, stay: nu
|
||||
if (at.arrival >= DAY_MINUTES) return { kind: 'nextDay' };
|
||||
if (!hours) return { kind: 'unknown' };
|
||||
if ('unread' in hours) return { kind: 'unread', text: hours.unread };
|
||||
const { source, windows } = hours;
|
||||
const { source, windows, continues } = hours;
|
||||
if (windows.length === 0) return { kind: 'closedAllDay', source };
|
||||
const leave = stay === null ? null : at.arrival + stay;
|
||||
const window = windows.find(([from, to]) => at.arrival >= from && at.arrival < to);
|
||||
const windowAfter = windows.some(([from]) => from > at.arrival);
|
||||
if (window) return fitInWindow(source, window, leave, { atLeast: at.atLeast, windowAfter });
|
||||
if (window) return fitInWindow(source, window, leave, { atLeast: at.atLeast, windowAfter, continues });
|
||||
return at.atLeast && windowAfter ? { kind: 'uncertain', source } : { kind: 'closed', source, windows };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user