Fix Schedule UI regressions and complete non-RU/EN locale translations

- Duration now sums segments + transfers (last arrival − first departure)
  for multi-leg/connecting in Schedule, matching TZ §4.1.14.3 and Angular.
- Default day auto-expands per TZ §4.1.14: current-week today, future-week
  first valid day, last-valid-day fallback when earlier days are out of
  window.
- Aircraft model no longer leaks into collapsed rows; shown only when
  expanded AND direct, mirroring Angular's
  operator-logo-and-model [showModel]="expanded && direct".
- Week tabs use MONTH-SHORT.* translation table so Russian renders
  "27 апр. - 3 май." instead of genitive "мая" from Intl.
- "Ранее искали" → "Вы искали" across all 9 locales (TZ §4.1.9.5).
- Sort-arrow headers compacted (inline-flex nowrap, zero gap) so labels
  stay on one line next to the chevrons.
- robots.txt allows Yandex/Googlebot/* with no Disallow (TZ §4.1.20).
- 6 non-RU/EN locales (de/es/fr/it/ja/ko) + zh were missing ~45 strings
  each; translated from Angular where present, hand-translated otherwise
  so every locale is down to the two intentional `.undefined` stubs.
This commit is contained in:
2026-04-22 17:02:31 +03:00
parent a9dacf0b97
commit 2e13d2d7ef
14 changed files with 3966 additions and 3923 deletions
+37 -5
View File
@@ -42,9 +42,9 @@ export interface FlightCardProps {
onViewDetails?: () => void;
/**
* Search direction. `arrival` swaps the boarding row to deboarding
* (label `Высадка` instead of `Посадка`); `schedule` keeps the
* aircraft model visible in the collapsed row per Angular's schedule
* results layout.
* (label `Высадка` instead of `Посадка`). Aircraft model is only shown
* in the expanded body for direct flights (matches Angular's
* `[showModel]="expanded && direct"` on operator-logo-and-model).
*/
direction?: "departure" | "arrival" | "route" | "flight" | "schedule";
/**
@@ -120,6 +120,28 @@ function formatFlyingTime(value: string, language: string): string {
return `${h}h ${m}m`;
}
function formatMinutes(total: number, language: string): string {
if (!Number.isFinite(total) || total < 0) return "";
const isRu = language.startsWith("ru");
const h = Math.floor(total / 60);
const m = total % 60;
if (isRu) return `${h}ч. ${m}мин.`;
return `${h}h ${m}m`;
}
function totalElapsedMinutes(flight: ISimpleFlight): number | null {
const legs = flight.routeType === "Direct" ? [flight.leg] : flight.legs;
const first = legs[0];
const last = legs[legs.length - 1];
if (!first || !last) return null;
const depUtc = first.departure.times.scheduledDeparture.utc;
const arrUtc = last.arrival.times.scheduledArrival.utc;
if (!depUtc || !arrUtc) return null;
const diffMs = Date.parse(arrUtc) - Date.parse(depUtc);
if (!Number.isFinite(diffMs) || diffMs <= 0) return null;
return Math.round(diffMs / 60000);
}
/**
* A single flight row in search results.
*
@@ -334,7 +356,7 @@ export const FlightCard: FC<FlightCardProps> = ({
>
<div className="flight-card__number" data-testid="flight-carrier-number">
<div>{flightNumber}</div>
{(expanded || direction === "schedule") && aircraftName && (
{expanded && flight.routeType === "Direct" && aircraftName && (
<div className="flight-card__aircraft">{aircraftName}</div>
)}
{/* TZ §4.1.13.3 Table 24 C15C16: route-changed / return-to-airport
@@ -393,7 +415,17 @@ export const FlightCard: FC<FlightCardProps> = ({
<div className="flight-card__duration" data-testid="flight-duration">
<span className="flight-card__duration-icon" aria-hidden="true" />
<span className="flight-card__duration-text">
{formatFlyingTime(flightDuration, language)}
{flight.routeType === "MultiLeg"
? (() => {
// TZ §4.1.14.3: total duration = segments + pauses.
// API's flyingTime is air-time only; use last-arrival minus
// first-departure elapsed time so connecting trips match Angular.
const mins = totalElapsedMinutes(flight);
return mins != null
? formatMinutes(mins, language)
: formatFlyingTime(flightDuration, language);
})()
: formatFlyingTime(flightDuration, language)}
</span>
</div>
) : (