diff --git a/client/src/components/Tours/TourDetailDialog.test.tsx b/client/src/components/Tours/TourDetailDialog.test.tsx index ab8b82876..a6cfc541f 100644 --- a/client/src/components/Tours/TourDetailDialog.test.tsx +++ b/client/src/components/Tours/TourDetailDialog.test.tsx @@ -192,7 +192,7 @@ describe('TourDetailDialog', () => { } const view = render() - 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() - 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$/ })) diff --git a/client/src/components/Tours/planner/TourWaypointList.test.tsx b/client/src/components/Tours/planner/TourWaypointList.test.tsx index a7b279266..5e03704bd 100644 --- a/client/src/components/Tours/planner/TourWaypointList.test.tsx +++ b/client/src/components/Tours/planner/TourWaypointList.test.tsx @@ -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(); - 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(); - 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(); }); diff --git a/client/src/components/Tours/walk/StationSchedule.test.tsx b/client/src/components/Tours/walk/StationSchedule.test.tsx index d095fdf8c..a96b7a16e 100644 --- a/client/src/components/Tours/walk/StationSchedule.test.tsx +++ b/client/src/components/Tours/walk/StationSchedule.test.tsx @@ -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', () => { diff --git a/client/src/components/Tours/walk/WalkDayLine.test.tsx b/client/src/components/Tours/walk/WalkDayLine.test.tsx index d7a93ec7a..2889ae785 100644 --- a/client/src/components/Tours/walk/WalkDayLine.test.tsx +++ b/client/src/components/Tours/walk/WalkDayLine.test.tsx @@ -44,7 +44,8 @@ describe('WalkDayLine', () => { render(); 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( ); - 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( { 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( ); - 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(); expect(screen.queryByText(/Opening hours from OpenStreetMap/)).not.toBeInTheDocument(); rerender(); diff --git a/client/src/components/Tours/walk/WalkDayLine.tsx b/client/src/components/Tours/walk/WalkDayLine.tsx index b031eb5bb..eb54a706c 100644 --- a/client/src/components/Tours/walk/WalkDayLine.tsx +++ b/client/src/components/Tours/walk/WalkDayLine.tsx @@ -15,7 +15,7 @@ type Translate = ReturnType['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 ( diff --git a/client/src/components/Tours/walk/WalkStations.test.tsx b/client/src/components/Tours/walk/WalkStations.test.tsx index 9e34c1f03..de95b4ad3 100644 --- a/client/src/components/Tours/walk/WalkStations.test.tsx +++ b/client/src/components/Tours/walk/WalkStations.test.tsx @@ -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(); }); diff --git a/server/src/nest/maps/maps-index.helpers.ts b/server/src/nest/maps/maps-index.helpers.ts index 3c031d63f..bf9ed09e3 100644 --- a/server/src/nest/maps/maps-index.helpers.ts +++ b/server/src/nest/maps/maps-index.helpers.ts @@ -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)); } diff --git a/server/tests/unit/db/repositories/TourWaypoints.repository.test.ts b/server/tests/unit/db/repositories/TourWaypoints.repository.test.ts index 373719211..9b457df21 100644 --- a/server/tests/unit/db/repositories/TourWaypoints.repository.test.ts +++ b/server/tests/unit/db/repositories/TourWaypoints.repository.test.ts @@ -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); diff --git a/server/tests/unit/db/repositories/Tours.repository.test.ts b/server/tests/unit/db/repositories/Tours.repository.test.ts index 602ea9121..bad29348b 100644 --- a/server/tests/unit/db/repositories/Tours.repository.test.ts +++ b/server/tests/unit/db/repositories/Tours.repository.test.ts @@ -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 }); diff --git a/server/tests/unit/nest/place-enrichment.description.test.ts b/server/tests/unit/nest/place-enrichment.description.test.ts index 2998eed56..f50874b7d 100644 --- a/server/tests/unit/nest/place-enrichment.description.test.ts +++ b/server/tests/unit/nest/place-enrichment.description.test.ts @@ -47,7 +47,8 @@ vi.mock('../../../src/utils/ssrfGuard', () => ({ const { mockTrekPlacesById } = vi.hoisted(() => ({ mockTrekPlacesById: vi.fn(async (_gers: string): Promise => null), })); -vi.mock('../../../src/nest/maps/trek-places.client', () => ({ +vi.mock('../../../src/nest/maps/trek-places.client', async (importOriginal) => ({ + ...(await importOriginal()), trekPlacesById: mockTrekPlacesById, })); diff --git a/server/tests/unit/nest/tour-library.mcp.test.ts b/server/tests/unit/nest/tour-library.mcp.test.ts index 144d91330..a96069278 100644 --- a/server/tests/unit/nest/tour-library.mcp.test.ts +++ b/server/tests/unit/nest/tour-library.mcp.test.ts @@ -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' }, ]); diff --git a/shared/src/i18n/ar/tours.ts b/shared/src/i18n/ar/tours.ts index fc5ba1c06..9bda580c6 100644 --- a/shared/src/i18n/ar/tours.ts +++ b/shared/src/i18n/ar/tours.ts @@ -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}', diff --git a/shared/src/i18n/az/tours.ts b/shared/src/i18n/az/tours.ts index 6762224dd..c1dc98c4b 100644 --- a/shared/src/i18n/az/tours.ts +++ b/shared/src/i18n/az/tours.ts @@ -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}', diff --git a/shared/src/i18n/br/tours.ts b/shared/src/i18n/br/tours.ts index 65cc0a690..2f9947266 100644 --- a/shared/src/i18n/br/tours.ts +++ b/shared/src/i18n/br/tours.ts @@ -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}', diff --git a/shared/src/i18n/ca/tours.ts b/shared/src/i18n/ca/tours.ts index 390c9eba9..abcdc884f 100644 --- a/shared/src/i18n/ca/tours.ts +++ b/shared/src/i18n/ca/tours.ts @@ -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}', diff --git a/shared/src/i18n/cs/tours.ts b/shared/src/i18n/cs/tours.ts index 91dc27370..6e03dba16 100644 --- a/shared/src/i18n/cs/tours.ts +++ b/shared/src/i18n/cs/tours.ts @@ -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}', diff --git a/shared/src/i18n/de/tours.ts b/shared/src/i18n/de/tours.ts index d110e9ee7..9f0e51342 100644 --- a/shared/src/i18n/de/tours.ts +++ b/shared/src/i18n/de/tours.ts @@ -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}', diff --git a/shared/src/i18n/en/tours.ts b/shared/src/i18n/en/tours.ts index d250c93e1..c114179aa 100644 --- a/shared/src/i18n/en/tours.ts +++ b/shared/src/i18n/en/tours.ts @@ -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}', diff --git a/shared/src/i18n/es/tours.ts b/shared/src/i18n/es/tours.ts index 13858566f..bceb3edd9 100644 --- a/shared/src/i18n/es/tours.ts +++ b/shared/src/i18n/es/tours.ts @@ -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}', diff --git a/shared/src/i18n/et/tours.ts b/shared/src/i18n/et/tours.ts index 2d96dc290..d2777b41b 100644 --- a/shared/src/i18n/et/tours.ts +++ b/shared/src/i18n/et/tours.ts @@ -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}', diff --git a/shared/src/i18n/fr/tours.ts b/shared/src/i18n/fr/tours.ts index 81fe08a99..30a1fe803 100644 --- a/shared/src/i18n/fr/tours.ts +++ b/shared/src/i18n/fr/tours.ts @@ -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}', diff --git a/shared/src/i18n/gr/tours.ts b/shared/src/i18n/gr/tours.ts index c588c2f40..8a9ba7890 100644 --- a/shared/src/i18n/gr/tours.ts +++ b/shared/src/i18n/gr/tours.ts @@ -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}', diff --git a/shared/src/i18n/hu/tours.ts b/shared/src/i18n/hu/tours.ts index 31dd12a41..98f09281b 100644 --- a/shared/src/i18n/hu/tours.ts +++ b/shared/src/i18n/hu/tours.ts @@ -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}', diff --git a/shared/src/i18n/id/tours.ts b/shared/src/i18n/id/tours.ts index 3898b6591..67aca3c3c 100644 --- a/shared/src/i18n/id/tours.ts +++ b/shared/src/i18n/id/tours.ts @@ -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}', diff --git a/shared/src/i18n/it/tours.ts b/shared/src/i18n/it/tours.ts index 460eac51c..ccef7ba0a 100644 --- a/shared/src/i18n/it/tours.ts +++ b/shared/src/i18n/it/tours.ts @@ -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}', diff --git a/shared/src/i18n/ja/tours.ts b/shared/src/i18n/ja/tours.ts index 889c92703..eeba6b001 100644 --- a/shared/src/i18n/ja/tours.ts +++ b/shared/src/i18n/ja/tours.ts @@ -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}', diff --git a/shared/src/i18n/ko/tours.ts b/shared/src/i18n/ko/tours.ts index 6fcadc5a1..f8c26a0d0 100644 --- a/shared/src/i18n/ko/tours.ts +++ b/shared/src/i18n/ko/tours.ts @@ -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}', diff --git a/shared/src/i18n/nl/tours.ts b/shared/src/i18n/nl/tours.ts index 38b82a0fa..8504f2e9b 100644 --- a/shared/src/i18n/nl/tours.ts +++ b/shared/src/i18n/nl/tours.ts @@ -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}', diff --git a/shared/src/i18n/pl/tours.ts b/shared/src/i18n/pl/tours.ts index c0550f294..fe48b87bb 100644 --- a/shared/src/i18n/pl/tours.ts +++ b/shared/src/i18n/pl/tours.ts @@ -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}', diff --git a/shared/src/i18n/ru/tours.ts b/shared/src/i18n/ru/tours.ts index 225d1f160..bb6b3ccec 100644 --- a/shared/src/i18n/ru/tours.ts +++ b/shared/src/i18n/ru/tours.ts @@ -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}', diff --git a/shared/src/i18n/sk/tours.ts b/shared/src/i18n/sk/tours.ts index 35e6098a9..8ad7f27f1 100644 --- a/shared/src/i18n/sk/tours.ts +++ b/shared/src/i18n/sk/tours.ts @@ -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}', diff --git a/shared/src/i18n/sv/tours.ts b/shared/src/i18n/sv/tours.ts index 06ccd739c..a8272be1a 100644 --- a/shared/src/i18n/sv/tours.ts +++ b/shared/src/i18n/sv/tours.ts @@ -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}', diff --git a/shared/src/i18n/th/tours.ts b/shared/src/i18n/th/tours.ts index e998cdaa0..5558452a2 100644 --- a/shared/src/i18n/th/tours.ts +++ b/shared/src/i18n/th/tours.ts @@ -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}', diff --git a/shared/src/i18n/tr/tours.ts b/shared/src/i18n/tr/tours.ts index 0759e4847..023f690c6 100644 --- a/shared/src/i18n/tr/tours.ts +++ b/shared/src/i18n/tr/tours.ts @@ -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}', diff --git a/shared/src/i18n/uk/tours.ts b/shared/src/i18n/uk/tours.ts index f77b7cd43..5e6a8dada 100644 --- a/shared/src/i18n/uk/tours.ts +++ b/shared/src/i18n/uk/tours.ts @@ -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}', diff --git a/shared/src/i18n/vi/tours.ts b/shared/src/i18n/vi/tours.ts index f03d8076b..ff0488e64 100644 --- a/shared/src/i18n/vi/tours.ts +++ b/shared/src/i18n/vi/tours.ts @@ -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}', diff --git a/shared/src/i18n/zh-TW/tours.ts b/shared/src/i18n/zh-TW/tours.ts index f29bc7664..dcda33da4 100644 --- a/shared/src/i18n/zh-TW/tours.ts +++ b/shared/src/i18n/zh-TW/tours.ts @@ -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} 出發', diff --git a/shared/src/i18n/zh/tours.ts b/shared/src/i18n/zh/tours.ts index da99ca533..de58b1cf5 100644 --- a/shared/src/i18n/zh/tours.ts +++ b/shared/src/i18n/zh/tours.ts @@ -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} 出发', diff --git a/shared/src/tours/walk-hours.spec.ts b/shared/src/tours/walk-hours.spec.ts index 390e86f4a..f5af1d3eb 100644 --- a/shared/src/tours/walk-hours.spec.ts +++ b/shared/src/tours/walk-hours.spec.ts @@ -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', () => { diff --git a/shared/src/tours/walk-hours.ts b/shared/src/tours/walk-hours.ts index b122f348b..cd7871ad5 100644 --- a/shared/src/tours/walk-hours.ts +++ b/shared/src/tours/walk-hours.ts @@ -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; 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 }; }