mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
The route proxy also refuses with 'routing busy — too many routes at once' (429) and 'route proxy error'; both showed in English in every locale. Mark them in ROUTING_REFUSALS and add es, ru, zh-CN and zh-Hant entries. errorText is now cleared on a new connection, a mode switch and stop(), so it never outlives the error it describes. Tests cover the two refusals, 'Routing failed: rate limited' (pattern order) and the stop() reset.
268 lines
8.9 KiB
JavaScript
268 lines
8.9 KiB
JavaScript
/**
|
|
* Layer status texts in the UI language.
|
|
*
|
|
* Layers report errors, loading labels and status notes as English data
|
|
* (stats, analyst records, what the model receives); the Layers panel, the
|
|
* loading chip, layer cards and the voice card show them translated through
|
|
* here, by their shape.
|
|
*
|
|
* Pure: imports only i18n and layer names, no DOM.
|
|
* @module ui/statusText
|
|
*/
|
|
import { sourceLabel } from './layerNames.js';
|
|
import { formatNumber, msg, t, tn, translateKnown } from '../i18n/index.js';
|
|
|
|
const STATUS_SEPARATOR = ' · ';
|
|
|
|
// Live-source failures arrive as English data (src/sources/live/contract.js,
|
|
// standalone.js, flights and military ingestion), some with a source name or a
|
|
// number in them; the row shows them in the UI language. Keep these in step
|
|
// with the producers. Anything else shows its catalog entry, or as it came.
|
|
const FEED_ERRORS = new Set([
|
|
msg('OpenSky OAuth client missing/invalid'),
|
|
msg('OpenSky OAuth rejected credentials'),
|
|
msg('OpenSky username/password rejected'),
|
|
msg('OpenSky auth missing'),
|
|
msg('OpenSky auth required'),
|
|
msg('OpenSky auth invalid'),
|
|
msg('OpenSky auth failed'),
|
|
]);
|
|
const FEED_ERROR_PATTERNS = [
|
|
// Street Level provider errors (providers/{kartaview,panoramax}/source.js),
|
|
// ahead of the generic "<source> HTTP <status>".
|
|
[
|
|
/^KartaView answered HTTP (\d+)$/,
|
|
([, status]) => t('KartaView answered HTTP {status}', { status }),
|
|
],
|
|
[
|
|
/^Panoramax answered HTTP (\d+)$/,
|
|
([, status]) => t('Panoramax answered HTTP {status}', { status }),
|
|
],
|
|
[
|
|
/^KartaView refused the request \(code (-?\d+)\)$/,
|
|
([, code]) => t('KartaView refused the request (code {code})', { code }),
|
|
],
|
|
// Weather imagery (weather/source.js, infraredImage.js) and vector tile
|
|
// (sources/vectorTiles.js) failures carry their own wording; the generic
|
|
// pattern below would take "Weather image" for a source name.
|
|
[
|
|
/^Weather (image )?HTTP (\d+)$/,
|
|
([, image, status]) =>
|
|
image
|
|
? t('Weather image HTTP {status}', { status })
|
|
: t('Weather HTTP {status}', { status }),
|
|
],
|
|
[
|
|
/^Vector tiles unavailable \(HTTP (\d+)\)$/,
|
|
([, status]) => t('Vector tiles unavailable (HTTP {status})', { status }),
|
|
],
|
|
// Layer statuses with a number or a feed name in them: GNSS
|
|
// (gnss/source.js), CelesTrak groups (satellites/ingestion.js) and transit
|
|
// feeds (transit/ingestion.js).
|
|
[
|
|
/^GNSS feed unavailable \(HTTP (\d{3})\)$/,
|
|
([, status]) => t('GNSS feed unavailable (HTTP {status})', { status }),
|
|
],
|
|
[
|
|
/^(\d+) CelesTrak groups? unavailable$/,
|
|
([, groups]) =>
|
|
tn(
|
|
Number(groups),
|
|
'{count} CelesTrak group unavailable',
|
|
'{count} CelesTrak groups unavailable',
|
|
{ count: formatNumber(Number(groups)) },
|
|
),
|
|
],
|
|
[
|
|
/^(.+) feed unavailable$/,
|
|
([, feed]) => t('{feed} feed unavailable', { feed: translateKnown(feed) }),
|
|
],
|
|
[
|
|
/^Source snapshot (\d+) min old$/,
|
|
([, minutes]) => t('Source snapshot {minutes} min old', { minutes }),
|
|
],
|
|
[
|
|
/^(.+) rate limited \(anonymous\)$/,
|
|
([, source]) =>
|
|
t('{source} rate limited (anonymous)', { source: sourceLabel(source) }),
|
|
],
|
|
[
|
|
/^(.+) rate limited$/,
|
|
([, source]) => t('{source} rate limited', { source: sourceLabel(source) }),
|
|
],
|
|
[
|
|
/^(.+) HTTP (\d{3})$/,
|
|
([, source, status]) =>
|
|
t('{source} HTTP {status}', { source: sourceLabel(source), status }),
|
|
],
|
|
[
|
|
/^(.+) network error$/,
|
|
([, source]) =>
|
|
t('{source} network error', { source: sourceLabel(source) }),
|
|
],
|
|
[
|
|
/^Malformed (.+) aircraft rows$/,
|
|
([, source]) =>
|
|
t('Malformed {source} aircraft rows', { source: sourceLabel(source) }),
|
|
],
|
|
[
|
|
/^Malformed (.+) response$/,
|
|
([, source]) =>
|
|
t('Malformed {source} response', { source: sourceLabel(source) }),
|
|
],
|
|
];
|
|
|
|
// Directions route failures (layers/directions/index.js), English data with
|
|
// the route proxy's refusal or an HTTP status in them. Ahead of the feed
|
|
// patterns, which would take "Routing failed: rate limited" for a source.
|
|
// The route proxy's refusal texts (server/providers/places/routes.js) show
|
|
// translated; any other text in {error} shows its catalog entry, or as given.
|
|
const ROUTING_REFUSALS = new Set([
|
|
msg('rate limited'),
|
|
msg('routing service is rate limited'),
|
|
msg('route leg too long'),
|
|
msg('route too long'),
|
|
msg('routing busy — too many routes at once'),
|
|
msg('route proxy error'),
|
|
]);
|
|
const routingRefusal = (text) =>
|
|
ROUTING_REFUSALS.has(text) ? t(text) : translateKnown(text);
|
|
const ROUTING_ERROR_PATTERNS = [
|
|
[
|
|
/^Routing unavailable \(HTTP (\d+)\)$/,
|
|
([, status]) => t('Routing unavailable (HTTP {status})', { status }),
|
|
],
|
|
[
|
|
/^Routing failed: (.+)$/,
|
|
([, error]) =>
|
|
t('Routing failed: {error}', { error: routingRefusal(error) }),
|
|
],
|
|
[
|
|
/^(.+) — try again in a moment$/,
|
|
([, error]) =>
|
|
t('{error} — try again in a moment', { error: routingRefusal(error) }),
|
|
],
|
|
];
|
|
|
|
const ERROR_PATTERNS = [...ROUTING_ERROR_PATTERNS, ...FEED_ERROR_PATTERNS];
|
|
|
|
/** One error text in the UI language, or null when nothing matches. */
|
|
function feedErrorText(text) {
|
|
if (FEED_ERRORS.has(text)) return t(text);
|
|
const known = translateKnown(text);
|
|
if (known !== text) return known;
|
|
for (const [pattern, format] of ERROR_PATTERNS) {
|
|
const match = pattern.exec(text);
|
|
if (match) return format(match);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* A layer's error text in the UI language (see FEED_ERRORS). An error made of
|
|
* several statuses joined by " · " (Local ADS-B) translates piece by piece,
|
|
* and so does a lone status piece ("feed 1090 unreachable").
|
|
*/
|
|
export function translateFeedError(text) {
|
|
if (typeof text !== 'string') return text;
|
|
const whole = feedErrorText(text);
|
|
if (whole !== null) return whole;
|
|
// feedErrorText has already looked the text up in the catalog.
|
|
if (!text.includes(STATUS_SEPARATOR)) return statusPatternText(text) ?? text;
|
|
return text
|
|
.split(STATUS_SEPARATOR)
|
|
.map((piece) => feedErrorText(piece) ?? statusPatternText(piece) ?? piece)
|
|
.join(STATUS_SEPARATOR);
|
|
}
|
|
|
|
// A decoder feed with no band is named by the server's "entry N" (its place
|
|
// in LOCAL_RECEIVER_FEEDS, server/providers/local-receivers.js).
|
|
const FEED_ENTRY = /^entry (\d+)$/;
|
|
|
|
function shownFeedName(name) {
|
|
const entry = FEED_ENTRY.exec(name);
|
|
return entry ? t('entry {n}', { n: entry[1] }) : name;
|
|
}
|
|
|
|
// Feed states named in a Local ADS-B feed problem ("feeds 1090, 978 stale").
|
|
const FEED_PROBLEM_FORMS = Object.freeze({
|
|
stale: (count, names) =>
|
|
tn(count, 'feed {names} stale', 'feeds {names} stale', { names }),
|
|
unreachable: (count, names) =>
|
|
tn(count, 'feed {names} unreachable', 'feeds {names} unreachable', {
|
|
names,
|
|
}),
|
|
invalid: (count, names) =>
|
|
tn(count, 'feed {names} invalid', 'feeds {names} invalid', { names }),
|
|
});
|
|
|
|
// Working texts with a number or a name in them arrive as English data, one
|
|
// piece of a " · " joined line each: transit (transit/queries.js) and Local
|
|
// ADS-B (localAdsb/status.js). The Layers panel and the voice card's analyst
|
|
// notes show them through here. Keep these in step with the producers.
|
|
const STATUS_PIECE_PATTERNS = [
|
|
[/^Loading (.+)$/, ([, feeds]) => t('Loading {feeds}', { feeds })],
|
|
[
|
|
/^(\d+) heard$/,
|
|
([, heard]) =>
|
|
tn(Number(heard), '{count} heard', '{count} heard', {
|
|
count: formatNumber(Number(heard)),
|
|
}),
|
|
],
|
|
[
|
|
/^(USB )?(\d+(?:\.\d+)?) msg\/s$/,
|
|
([, usb, rate]) =>
|
|
usb
|
|
? t('USB {rate} msg/s', { rate: formatNumber(Number(rate)) })
|
|
: t('{rate} msg/s', { rate: formatNumber(Number(rate)) }),
|
|
],
|
|
[
|
|
/^(\d+) feeds? live$/,
|
|
([, live]) =>
|
|
tn(Number(live), '{count} feed live', '{count} feeds live', {
|
|
count: formatNumber(Number(live)),
|
|
}),
|
|
],
|
|
[
|
|
/^feeds? (.+) (stale|unreachable|invalid)$/,
|
|
([, names, status]) => {
|
|
const list = names.split(', ');
|
|
return FEED_PROBLEM_FORMS[status](
|
|
list.length,
|
|
list.map(shownFeedName).join(', '),
|
|
);
|
|
},
|
|
],
|
|
// A quiet transit feed in the coverage line, "TTC 0 (95s old)"
|
|
// (transit/queries.js); the feed name and the numbers stay as they came.
|
|
[
|
|
/^(.+) (\d+) \((\d+)s old\)$/,
|
|
([, feed, count, seconds]) =>
|
|
t('{feed} {count} ({seconds}s old)', { feed, count, seconds }),
|
|
],
|
|
];
|
|
|
|
/** A status piece by its shape in the UI language, or null. */
|
|
function statusPatternText(piece) {
|
|
for (const [pattern, format] of STATUS_PIECE_PATTERNS) {
|
|
const match = pattern.exec(piece);
|
|
if (match) return format(match);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** One piece of a status line in the UI language, or as it came. */
|
|
function translateStatusPiece(piece) {
|
|
const known = translateKnown(piece);
|
|
if (known !== piece) return known;
|
|
return statusPatternText(piece) ?? piece;
|
|
}
|
|
|
|
/** A status line of " · " joined pieces in the UI language, piece by piece. */
|
|
export function translateStatusLine(text) {
|
|
return text
|
|
.split(STATUS_SEPARATOR)
|
|
.map(translateStatusPiece)
|
|
.join(STATUS_SEPARATOR);
|
|
}
|