Fix three parity issues from final audit

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.
This commit is contained in:
2026-04-18 14:10:26 +03:00
parent 4e91e9dca1
commit cb61cafbf1
4 changed files with 69 additions and 14 deletions
@@ -185,27 +185,40 @@ export const OnlineBoardSearchPage: FC<OnlineBoardSearchPageProps> = ({
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}`;
@@ -84,14 +84,16 @@ export const ScheduleSearchPage: FC<ScheduleSearchPageProps> = ({ 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);
+31
View File
@@ -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 <Outlet />.
@@ -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);
+11 -2
View File
@@ -77,7 +77,13 @@ export const FlightCard: FC<FlightCardProps> = ({ flight, onClick }) => {
<TimeGroup
scheduled={depTimes.scheduledDeparture.local}
actual={depTimes.actualBlockOff?.local}
dayChange={depTimes.actualBlockOff?.dayChange.value}
// Prefer the actual day-offset but fall back to the scheduled one
// so scheduled-only flights still show the '+1' marker when they
// cross midnight (SU 6805 departs at 23:30 and arrives at 00:55+1).
dayChange={
depTimes.actualBlockOff?.dayChange.value ??
depTimes.scheduledDeparture.dayChange?.value
}
/>
</div>
@@ -99,7 +105,10 @@ export const FlightCard: FC<FlightCardProps> = ({ flight, onClick }) => {
<TimeGroup
scheduled={arrTimes.scheduledArrival.local}
actual={arrTimes.actualBlockOn?.local}
dayChange={arrTimes.actualBlockOn?.dayChange.value}
dayChange={
arrTimes.actualBlockOn?.dayChange.value ??
arrTimes.scheduledArrival.dayChange?.value
}
/>
</div>