From cb61cafbf15f952b2009a9f072bdf13844a274c5 Mon Sep 17 00:00:00 2001 From: gnezim Date: Sat, 18 Apr 2026 14:10:26 +0300 Subject: [PATCH] Fix three parity issues from final audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Route heading uses airport name when a code maps only to an airport (SVO → 'Шереметьево') but prefers the city when the code is a city too (LED → 'Санкт-Петербург', not 'Пулково'). Angular does the same. Apply the new lookup order in both the onlineboard and schedule search pages. 2. Append ', Сегодня' (or 'DD.MM.YYYY' for other dates) to the board search heading, matching Angular. 3. Render the '+1' day-change marker on FlightCard even when only scheduled times are known. Previously the fallback pulled the value from `actualBlockOff/On.dayChange`, which is undefined for scheduled-only flights — so overnight flights like SU 6805 (23:30 → 00:55 +1) showed no indicator. Read `scheduledDeparture/Arrival.dayChange.value` when the actual block time is missing. 4. Localize the PrimeReact Calendar widget: register a Russian locale in [lang]/layout.tsx and set the active one on every locale change, so 'Choose Date' reads 'Выбрать дату' and month/day names localize. --- .../components/OnlineBoardSearchPage.tsx | 27 +++++++++++----- .../components/ScheduleSearchPage.tsx | 12 ++++--- src/routes/[lang]/layout.tsx | 31 +++++++++++++++++++ src/ui/flights/FlightCard.tsx | 13 ++++++-- 4 files changed, 69 insertions(+), 14 deletions(-) diff --git a/src/features/online-board/components/OnlineBoardSearchPage.tsx b/src/features/online-board/components/OnlineBoardSearchPage.tsx index 1c7d7c74..8784d092 100644 --- a/src/features/online-board/components/OnlineBoardSearchPage.tsx +++ b/src/features/online-board/components/OnlineBoardSearchPage.tsx @@ -185,27 +185,40 @@ export const OnlineBoardSearchPage: FC = ({ const lang = routeParams.lang ?? "ru"; const { dictionaries } = useDictionaries(lang); - // Human-readable title/breadcrumb. Angular derives these from the - // station dictionary — e.g. "Маршрут: Шереметьево - Санкт-Петербург". + // Human-readable title/breadcrumb. Angular prefers the city name when a + // code resolves to a city (LED → 'Санкт-Петербург'); falls back to the + // airport name only for codes that aren't city codes (SVO → 'Шереметьево'). const describeStation = (code?: string): string => { if (!code || !dictionaries) return code ?? ""; const upper = code.toUpperCase(); - return ( - dictionaries.airportByCode.get(upper)?.name ?? - dictionaries.cityByCode.get(upper)?.name ?? - code - ); + const city = dictionaries.cityByCode.get(upper); + if (city) return city.name; + const airport = dictionaries.airportByCode.get(upper); + if (airport) return airport.name; + return code; }; + // Today's date gets rendered as 'Сегодня', matching Angular's heading. + const dateLabel = ((): string => { + if (!params.date || params.date.length !== 8) return ""; + const now = new Date(); + const todayYyyymmdd = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}`; + if (params.date === todayYyyymmdd) return t("SHARED.TODAY"); + // Otherwise format as 'DD.MM.YYYY'. + return `${params.date.slice(6, 8)}.${params.date.slice(4, 6)}.${params.date.slice(0, 4)}`; + })(); let searchHeading: string; switch (params.type) { case "route": searchHeading = `${t("BOARD.ROUTE-TEXT")}${describeStation(params.departure)} - ${describeStation(params.arrival)}`; + if (dateLabel) searchHeading += `, ${dateLabel}`; break; case "departure": searchHeading = `${t("BOARD.DEPARTURE")}: ${describeStation(params.station)}`; + if (dateLabel) searchHeading += `, ${dateLabel}`; break; case "arrival": searchHeading = `${t("BOARD.ARRIVAL")}: ${describeStation(params.station)}`; + if (dateLabel) searchHeading += `, ${dateLabel}`; break; case "flight": searchHeading = `${t("BOARD.FLIGHT_NUMBER")}: ${params.carrier}${params.flightNumber}`; diff --git a/src/features/schedule/components/ScheduleSearchPage.tsx b/src/features/schedule/components/ScheduleSearchPage.tsx index 484f96ea..d15dff4d 100644 --- a/src/features/schedule/components/ScheduleSearchPage.tsx +++ b/src/features/schedule/components/ScheduleSearchPage.tsx @@ -84,14 +84,16 @@ export const ScheduleSearchPage: FC = ({ params }) => { // Resolve IATA codes to human city/airport names so the heading reads // 'Маршрут: Шереметьево - Санкт-Петербург' instead of 'SVO - LED'. + // City wins over airport when the code resolves to both (Angular + // parity — LED is both codes; arrivals render the city name). const describeStation = (code?: string): string => { if (!code || !dictionaries) return code ?? ""; const upper = code.toUpperCase(); - return ( - dictionaries.airportByCode.get(upper)?.name ?? - dictionaries.cityByCode.get(upper)?.name ?? - code - ); + const city = dictionaries.cityByCode.get(upper); + if (city) return city.name; + const airport = dictionaries.airportByCode.get(upper); + if (airport) return airport.name; + return code; }; const depName = describeStation(outbound.departure); const arrName = describeStation(outbound.arrival); diff --git a/src/routes/[lang]/layout.tsx b/src/routes/[lang]/layout.tsx index 2ce29b10..04d9c898 100644 --- a/src/routes/[lang]/layout.tsx +++ b/src/routes/[lang]/layout.tsx @@ -1,11 +1,39 @@ import { useState, useEffect } from "react"; import { useParams } from "@modern-js/runtime/router"; import { Outlet } from "@modern-js/runtime/router"; +import { addLocale, locale as setPrimeLocale } from "primereact/api"; import { isLanguage, type Language } from "@/i18n/resolver"; import { createI18nInstance } from "@/i18n/config"; import { I18nProvider } from "@/i18n/provider"; import type i18next from "i18next"; +// Register PrimeReact locales once at module load so the Calendar / +// AutoComplete widgets render with localized labels (e.g. 'Выбрать дату' +// instead of 'Choose Date'). Only the keys PrimeReact actually reads +// are listed here; the rest fall back to defaults. +addLocale("ru", { + dayNames: ["воскресенье", "понедельник", "вторник", "среда", "четверг", "пятница", "суббота"], + dayNamesShort: ["вс", "пн", "вт", "ср", "чт", "пт", "сб"], + dayNamesMin: ["вс", "пн", "вт", "ср", "чт", "пт", "сб"], + monthNames: ["январь", "февраль", "март", "апрель", "май", "июнь", "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"], + monthNamesShort: ["янв", "фев", "мар", "апр", "май", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"], + today: "Сегодня", + clear: "Очистить", + chooseDate: "Выбрать дату", + prevDecade: "Предыдущее десятилетие", + nextDecade: "Следующее десятилетие", + prevYear: "Предыдущий год", + nextYear: "Следующий год", + prevMonth: "Предыдущий месяц", + nextMonth: "Следующий месяц", + chooseYear: "Выбрать год", + chooseMonth: "Выбрать месяц", + weekHeader: "Нед", + firstDayOfWeek: 1, + emptyMessage: "Совпадений не найдено", + emptyFilterMessage: "Совпадений не найдено", +}); + /** * Locale-scoped layout. Validates the `lang` URL segment, * creates the i18n instance, and wraps children via . @@ -22,6 +50,9 @@ export default function LangLayout(): JSX.Element { useEffect(() => { if (!locale) return; let cancelled = false; + // PrimeReact reads the active locale via its module-level state; set it + // whenever our URL locale changes so widgets pick up the new labels. + setPrimeLocale(locale === "ru" ? "ru" : "en"); void createI18nInstance({ locale }).then((instance) => { if (!cancelled) { setI18n(instance); diff --git a/src/ui/flights/FlightCard.tsx b/src/ui/flights/FlightCard.tsx index ec58b67b..0c6f582c 100644 --- a/src/ui/flights/FlightCard.tsx +++ b/src/ui/flights/FlightCard.tsx @@ -77,7 +77,13 @@ export const FlightCard: FC = ({ flight, onClick }) => { @@ -99,7 +105,10 @@ export const FlightCard: FC = ({ flight, onClick }) => {