Close the remaining high-impact parity gaps

Batch of fixes identified by the comparison audit:

Schedule search page (ScheduleSearchPage):
- Resolve IATA codes to city/airport names, so the H1 reads
  'Маршрут: Шереметьево - Санкт-Петербург' instead of 'SVO - LED'.
- Breadcrumb trail now includes the human-friendly route as its
  last entry.

Details page (OnlineBoardDetailsPage):
- Hide the 'Перелет N' leg header for single-leg flights (Angular
  parity — that label is only meaningful for multi-leg routes).
- Translate the leg status through FLIGHT-STATUSES.* instead of
  emitting the raw enum ('Cancelled' → 'Отменен', etc.).
- Humanize leg and total flying time through formatDuration so the
  page reads '1ч 25м' rather than '01:25:00'.

Details meal panel (MealPanel):
- Use the same FOOD.* translation keys as Angular, so labels become
  'Эконом класс / Комфорт класс / Бизнес класс / Специальное
  питание'.
- Add the Special-meal icon + link (was stubbed out previously).

Accessibility:
- Route the English aria-labels through new SHARED.A11Y-* keys in
  DayTabs pagination, FlightListSkeleton, ScrollUpButton and
  PrintButton.

Breadcrumbs:
- Render the 'Главная' crumb as a link even when it's the only /
  last item (it was dropping to plain text on start pages). Angular
  always links it to aeroflot.ru.

Tests updated to assert the new translated labels and duration
formatting; 1258 tests passing.
This commit is contained in:
2026-04-18 13:27:56 +03:00
parent 96235d5534
commit 0c660671ea
23 changed files with 194 additions and 49 deletions
+3 -1
View File
@@ -1,4 +1,5 @@
import type { FC } from "react";
import { useTranslation } from "@/i18n/provider.js";
import "./FlightListSkeleton.scss";
export interface FlightListSkeletonProps {
@@ -13,8 +14,9 @@ export interface FlightListSkeletonProps {
export const FlightListSkeleton: FC<FlightListSkeletonProps> = ({
count = 5,
}) => {
const { t } = useTranslation();
return (
<div className="flight-list-skeleton" aria-busy="true" aria-label="Loading flights">
<div className="flight-list-skeleton" aria-busy="true" aria-label={t("SHARED.A11Y-LOADING-FLIGHTS")}>
{Array.from({ length: count }, (_, i) => (
<div key={i} className="flight-list-skeleton__row">
<div className="flight-list-skeleton__cell flight-list-skeleton__cell--number" />
+22 -15
View File
@@ -31,21 +31,28 @@ export const Breadcrumbs: FC<BreadcrumbsProps> = ({ items = [] }) => {
return (
<nav className="breadcrumbs" aria-label="breadcrumb" data-testid="breadcrumbs">
<ol className="breadcrumbs__list">
{allItems.map((item, index) => (
<li
key={`${item.label}-${index}`}
className={`breadcrumbs__item${index === allItems.length - 1 ? " breadcrumbs__item--active" : ""}`}
>
{item.url && index < allItems.length - 1 ? (
<a href={item.url} className="breadcrumbs__link">{item.label}</a>
) : (
<span className="breadcrumbs__text">{item.label}</span>
)}
{index < allItems.length - 1 && (
<span className="breadcrumbs__separator">&gt;</span>
)}
</li>
))}
{allItems.map((item, index) => {
const isLast = index === allItems.length - 1;
// The Home crumb (index 0) is always a link, even on start pages
// where it happens to be the only/last item — matches Angular,
// which always renders 'Главная' with the aeroflot.ru href.
const showAsLink = Boolean(item.url) && (index === 0 || !isLast);
return (
<li
key={`${item.label}-${index}`}
className={`breadcrumbs__item${isLast ? " breadcrumbs__item--active" : ""}`}
>
{showAsLink ? (
<a href={item.url} className="breadcrumbs__link">{item.label}</a>
) : (
<span className="breadcrumbs__text">{item.label}</span>
)}
{!isLast && (
<span className="breadcrumbs__separator">&gt;</span>
)}
</li>
);
})}
</ol>
</nav>
);
+3 -1
View File
@@ -7,11 +7,13 @@
*/
import { type FC, useState, useEffect, useCallback } from "react";
import { useTranslation } from "@/i18n/provider.js";
import "./ScrollUpButton.scss";
const SCROLL_THRESHOLD = 300;
export const ScrollUpButton: FC = () => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
useEffect(() => {
@@ -34,7 +36,7 @@ export const ScrollUpButton: FC = () => {
type="button"
className="scroll-up-button"
onClick={scrollToTop}
aria-label="Scroll to top"
aria-label={t("SHARED.A11Y-SCROLL-TO-TOP")}
data-testid="scroll-up-button"
>
<svg viewBox="0 0 24 24" width="24" height="24">