Say why Transit and Bike Share rows are empty outside coverage

Over a city with no feed or system (London, Tokyo) both rows read
"ON · — · never", the same as a feed that never answered. Transit already
computed a coverage note but put it only in `coverage`, which the row does
not show; Bike Share reported nothing at all.

Both layers now report a `zoom-in` guidance status with a `statusMessage`:
"Fly below 3,000 km to a covered region" / "No feed here yet · N regions
available" for Transit, and "Fly below 48 km to a covered city" / "No bike
share here yet · N systems available" for Bike Share. A loading or failed
Bike Share city keeps its own text. Leaving coverage clears the layer's
last-update time, so the row no longer dates data it no longer draws.

The coverage-note translations move from the voice card into
`translateStatusMessage` (ui/statusText.js), which the Layers row and the
voice card now share.
This commit is contained in:
German Khokhlov committed 2026-11-05 12:52:28 +04:00
1 parent 7d8b80db0a
commit b6ad32009f
23 files changed
+459 -37

No files matched your search

+9
View File
@@ -2912,6 +2912,15 @@
"many": "لا مصدر بيانات هنا بعد · {n} منطقة متاحة",
"other": "لا مصدر بيانات هنا بعد · {n} منطقة متاحة"
},
"Fly below {km} km to a covered city": "انخفض إلى ما دون {km} كم فوق مدينة مغطاة",
"No bike share here yet · {n} systems available": {
"zero": "لا دراجات مشتركة هنا بعد · {n} نظام متاح",
"one": "لا دراجات مشتركة هنا بعد · نظام واحد متاح ({n})",
"two": "لا دراجات مشتركة هنا بعد · نظامان متاحان ({n})",
"few": "لا دراجات مشتركة هنا بعد · {n} أنظمة متاحة",
"many": "لا دراجات مشتركة هنا بعد · {n} نظامًا متاحًا",
"other": "لا دراجات مشتركة هنا بعد · {n} نظام متاح"
},
"degraded": "متدهور",
"stale": "متقادم",
"partial": "جزئي",
+5
View File
@@ -2662,6 +2662,11 @@
"one": "Hier noch kein Feed · {n} Region verfügbar",
"other": "Hier noch kein Feed · {n} Regionen verfügbar"
},
"Fly below {km} km to a covered city": "Unter {km} km Höhe zu einer abgedeckten Stadt fliegen",
"No bike share here yet · {n} systems available": {
"one": "Hier noch kein Bikesharing · {n} System verfügbar",
"other": "Hier noch kein Bikesharing · {n} Systeme verfügbar"
},
"degraded": "eingeschränkt",
"stale": "veraltet",
"partial": "teilweise",
+6
View File
@@ -2821,6 +2821,12 @@
"many": "Aún no hay fuente aquí · {n} de regiones disponibles",
"other": "Aún no hay fuente aquí · {n} regiones disponibles"
},
"Fly below {km} km to a covered city": "Desciende por debajo de {km} km sobre una ciudad con cobertura",
"No bike share here yet · {n} systems available": {
"one": "Aún no hay bicis compartidas aquí · {n} sistema disponible",
"many": "Aún no hay bicis compartidas aquí · {n} de sistemas disponibles",
"other": "Aún no hay bicis compartidas aquí · {n} sistemas disponibles"
},
"Wind speed field unavailable": "Campo de velocidad del viento no disponible",
"Decoder feeds": "Fuentes del decodificador",
"WebUSB + decoder feeds": "WebUSB + fuentes del decodificador",
+6
View File
@@ -2531,6 +2531,12 @@
"many": "Pas encore de flux ici · {n} de régions disponibles",
"other": "Pas encore de flux ici · {n} régions disponibles"
},
"Fly below {km} km to a covered city": "Descendez sous {km} km au-dessus d'une ville couverte",
"No bike share here yet · {n} systems available": {
"one": "Pas encore de vélos partagés ici · {n} système disponible",
"many": "Pas encore de vélos partagés ici · {n} de systèmes disponibles",
"other": "Pas encore de vélos partagés ici · {n} systèmes disponibles"
},
"degraded": "dégradé",
"stale": "obsolète",
"partial": "partiel",
+6
View File
@@ -2531,6 +2531,12 @@
"many": "Ancora nessun feed qui · {n} di regioni disponibili",
"other": "Ancora nessun feed qui · {n} regioni disponibili"
},
"Fly below {km} km to a covered city": "Scendi sotto {km} km su una città coperta",
"No bike share here yet · {n} systems available": {
"one": "Ancora nessun bike sharing qui · {n} sistema disponibile",
"many": "Ancora nessun bike sharing qui · {n} di sistemi disponibili",
"other": "Ancora nessun bike sharing qui · {n} sistemi disponibili"
},
"degraded": "degradato",
"stale": "obsoleto",
"partial": "parziale",
+4
View File
@@ -2401,6 +2401,10 @@
"No feed here yet · {n} regions available": {
"other": "ここにはまだフィードがありません · 利用可能な地域 {n}か所"
},
"Fly below {km} km to a covered city": "{km} km未満まで高度を下げ、カバー範囲内の都市へ移動してください",
"No bike share here yet · {n} systems available": {
"other": "ここにはまだシェアサイクルがありません · 利用可能なシステム {n}件"
},
"degraded": "性能低下",
"stale": "更新遅延",
"partial": "部分的",
+6
View File
@@ -2533,6 +2533,12 @@
"many": "Ainda não há feed aqui · {n} de regiões disponíveis",
"other": "Ainda não há feed aqui · {n} regiões disponíveis"
},
"Fly below {km} km to a covered city": "Desça abaixo de {km} km sobre uma cidade com cobertura",
"No bike share here yet · {n} systems available": {
"one": "Ainda não há bicicletas compartilhadas aqui · {n} sistema disponível",
"many": "Ainda não há bicicletas compartilhadas aqui · {n} de sistemas disponíveis",
"other": "Ainda não há bicicletas compartilhadas aqui · {n} sistemas disponíveis"
},
"degraded": "degradado",
"stale": "desatualizado",
"partial": "parcial",
+7
View File
@@ -2884,6 +2884,13 @@
"many": "Здесь пока нет потока данных · доступно {n} регионов",
"other": "Здесь пока нет потока данных · доступно {n} региона"
},
"Fly below {km} km to a covered city": "Опуститесь ниже {km} км над городом с покрытием",
"No bike share here yet · {n} systems available": {
"one": "Здесь пока нет проката велосипедов · доступна {n} система",
"few": "Здесь пока нет проката велосипедов · доступны {n} системы",
"many": "Здесь пока нет проката велосипедов · доступно {n} систем",
"other": "Здесь пока нет проката велосипедов · доступно {n} системы"
},
"Wind speed field unavailable": "Поле скорости ветра недоступно",
"Decoder feeds": "Потоки декодера",
"WebUSB + decoder feeds": "WebUSB + потоки декодера",
+7
View File
@@ -2595,6 +2595,13 @@
"many": "Тут поки немає потоку даних · доступно {n} регіонів",
"other": "Тут поки немає потоку даних · доступно {n} регіону"
},
"Fly below {km} km to a covered city": "Опустіться нижче {km} км над містом із покриттям",
"No bike share here yet · {n} systems available": {
"one": "Тут поки немає прокату велосипедів · доступна {n} система",
"few": "Тут поки немає прокату велосипедів · доступні {n} системи",
"many": "Тут поки немає прокату велосипедів · доступно {n} систем",
"other": "Тут поки немає прокату велосипедів · доступно {n} системи"
},
"degraded": "з перебоями",
"stale": "застарів",
"partial": "частково",
+4
View File
@@ -2302,6 +2302,10 @@
"No feed here yet · {n} regions available": {
"other": "此处暂无数据源 · 有 {n} 个覆盖区域"
},
"Fly below {km} km to a covered city": "请降至 {km} km 以下并飞往有覆盖的城市",
"No bike share here yet · {n} systems available": {
"other": "此处暂无共享单车 · 有 {n} 个可用系统"
},
"degraded": "降级",
"stale": "过期",
"partial": "部分可用",
+4
View File
@@ -2290,6 +2290,10 @@
"No feed here yet · {n} regions available": {
"other": "此處暫無數據源 · 有 {n} 個覆蓋區域"
},
"Fly below {km} km to a covered city": "請降至 {km} km 以下並飛往有覆蓋的城市",
"No bike share here yet · {n} systems available": {
"other": "此處暫無共享單車 · 有 {n} 個可用系統"
},
"degraded": "降級",
"stale": "過期",
"partial": "部分可用",
+20 -2
View File
@@ -1,5 +1,6 @@
import { msg, t } from '../../i18n/index.js';
import { STATUS_POLL_MS } from './policy.js';
import { ACTIVATION_ENTER_ALTITUDE_M, STATUS_POLL_MS } from './policy.js';
import { GBFS_CITY_REGISTRY } from './registry.js';
export function createControls({ state: layerState, services, parts, source }) {
const methods = {
@@ -57,7 +58,7 @@ export function createControls({ state: layerState, services, parts, source }) {
/**
* Return current layer statistics for the UI status display.
* @returns {{ count: number, lastUpdate: number|null, loading: boolean, loadingLabel?: string, error?: string, degraded?: boolean, stale?: boolean }}
* @returns {{ count: number, lastUpdate: number|null, loading: boolean, loadingLabel?: string, error?: string, degraded?: boolean, stale?: boolean, status?: string, statusMessage?: string }}
*/
getStats() {
const stats = {
@@ -65,6 +66,23 @@ export function createControls({ state: layerState, services, parts, source }) {
lastUpdate: layerState._lastUpdate,
loading: layerState._loading,
};
// No system in range is guidance, not an empty feed: the camera is too
// high, or no catalogued system is near the view. A city that failed
// leaves range empty too, and reports its failure below instead.
if (
layerState._enabled &&
!layerState._loading &&
layerState._activeCityIds.size === 0 &&
layerState._failedCityIds.size === 0
) {
// English data (the analyst's empty note to the model, and the Layers
// row's prompt); both show it through translateStatusMessage
// (ui/statusText.js).
const note = layerState._altitudeGateEnabled
? `No bike share here yet · ${GBFS_CITY_REGISTRY.length} systems available`
: `Fly below ${Math.round(ACTIVATION_ENTER_ALTITUDE_M / 1000).toLocaleString('en-US')} km to a covered city`;
return { ...stats, status: 'zoom-in', statusMessage: note };
}
if (layerState._loading) {
stats.loadingLabel =
layerState._activeCityIds.size > 0
@@ -0,0 +1,124 @@
// BIKESHARE COVERAGE STATUS — a view with no system says so.
//
// Over a city with no catalogued system (London, Tokyo) the layer drew nothing
// and reported nothing, so its row read "ON · — · GBFS · never": the same as a
// feed that had never answered. These cases pin the rule that replaced it:
// with no system in range the layer reports a guidance status that says why,
// and a view it left keeps no date for data it no longer draws.
//
// Run with: npm test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createState } from './state.js';
import { createControls } from './controls.js';
import { createModel } from './model.js';
import { createViewport } from './viewport.js';
import { GBFS_CITY_REGISTRY } from './registry.js';
const LONDON = { lat: 51.5072, lon: -0.1276 };
function makeLayer() {
const services = {
overlays: {
setOverlayEntries() {},
setOverlaySourceVisible() {},
clearOverlaySource() {},
},
render: { governorRequestRender() {} },
};
const state = createState({ services });
const parts = {
queries: { collectDetectableStations: () => [] },
model: createModel({ state, services, parts: {}, source: null }),
ingestion: { abortInFlight() {} },
rendering: { removeCityPoints() {} },
};
parts.viewport = createViewport({ state, services, parts, source: null });
const { methods } = createControls({ state, services, parts, source: {} });
state._enabled = true;
return { state, methods, viewport: parts.viewport };
}
/** A viewer looking straight down at `at` from `height` metres. */
function viewerAt(at, height) {
const rad = Math.PI / 180;
return {
camera: {
positionCartographic: {
height,
latitude: at.lat * rad,
longitude: at.lon * rad,
},
computeViewRectangle: () => undefined,
},
scene: { globe: {} },
};
}
test('above the altitude gate the row says how low to fly', async () => {
const { state, methods, viewport } = makeLayer();
state._viewer = viewerAt(LONDON, 2_000_000);
await viewport.runProximityCheck();
const stats = methods.getStats();
assert.equal(stats.status, 'zoom-in');
assert.equal(stats.statusMessage, 'Fly below 48 km to a covered city');
assert.equal(stats.error, undefined);
});
test('a city with no system says so and counts the ones there are', async () => {
const { state, methods, viewport } = makeLayer();
state._viewer = viewerAt(LONDON, 8_000);
await viewport.runProximityCheck();
const stats = methods.getStats();
assert.equal(stats.status, 'zoom-in');
assert.equal(
stats.statusMessage,
`No bike share here yet · ${GBFS_CITY_REGISTRY.length} systems available`,
);
assert.equal(stats.count, 0);
});
test('a city with a system in range is not given the prompt', () => {
const { state, methods } = makeLayer();
state._altitudeGateEnabled = true;
state._activeCityIds.add('boston-bluebikes');
assert.equal(methods.getStats().status, undefined);
});
test('a loading or failed city reports itself, not the prompt', () => {
const { state, methods } = makeLayer();
state._altitudeGateEnabled = true;
state._loading = true;
assert.equal(methods.getStats().status, undefined);
// A city that failed is dropped from range, and still owns the row.
state._loading = false;
state._failedCityIds.add('boston-bluebikes');
const stats = methods.getStats();
assert.equal(stats.status, undefined);
assert.equal(stats.error, 'GBFS fetch error');
});
test('a disabled layer reports no prompt', () => {
const { state, methods } = makeLayer();
state._enabled = false;
assert.equal(methods.getStats().status, undefined);
});
test('leaving every system behind drops the date of the data it drew', async () => {
for (const height of [8_000, 2_000_000]) {
const { state, methods, viewport } = makeLayer();
state._lastUpdate = Date.now();
// Boston's stations were drawn and dated; the camera moves to London.
state._activeCityIds.add('boston-bluebikes');
state._cityRuntime.set('boston-bluebikes', {});
state._viewer = viewerAt(LONDON, height);
await viewport.runProximityCheck();
assert.equal(state._activeCityIds.size, 0);
assert.equal(methods.getStats().lastUpdate, null, `at ${height} m`);
}
});
+3
View File
@@ -125,6 +125,8 @@ export function createViewport({ state: layerState, services, parts, source }) {
// A failure belongs to a city in range; none are once everything is off.
layerState._failedCityIds.clear();
layerState._count = 0;
// Nothing is left on the map to date.
layerState._lastUpdate = null;
}
/**
@@ -227,6 +229,7 @@ export function createViewport({ state: layerState, services, parts, source }) {
layerState._activeCityIds = nextActive;
if (layerState._activeCityIds.size === 0) {
layerState._count = 0;
layerState._lastUpdate = null;
return;
}
+39
View File
@@ -3982,3 +3982,42 @@ test('parked transit reprojects held and reported courses at the bounded camera
'selected unknown course also stays screen-up',
);
});
test('a view with no feed reads its coverage prompt, not an undated feed', async (t) => {
const app = harness(t);
app.serve('mbta', () => ({
status: 200,
body: snapshot(
'mbta',
'MBTA',
[vehicle('bus-1', 42.36, -71.06, Date.now() / 1000)],
{ fetchedAt: Date.now() },
),
}));
app.layer.enable(app.viewer);
await app.layer.update();
assert.ok(app.layer.getStats().lastUpdate, 'Boston has a dated snapshot');
// London has no registered feed: the row prompts instead of dating Boston's
// last snapshot over an empty map.
app.lookAt({ west: -0.2, south: 51.45, east: -0.05, north: 51.55 });
app.advance(1_000);
const away = app.layer.getStats();
assert.equal(away.status, 'zoom-in');
assert.equal(away.count, 0);
assert.equal(away.lastUpdate, null);
assert.match(
away.statusMessage,
/^No feed here yet · \d+ regions available$/,
);
assert.equal(away.coverage, away.statusMessage);
// Above the altitude gate the prompt says how low to fly.
app.viewer.camera.positionCartographic.height = 10_000_000;
app.lookAt({ west: -0.2, south: 51.45, east: -0.05, north: 51.55 });
app.advance(1_000);
assert.equal(
app.layer.getStats().statusMessage,
'Fly below 3,000 km to a covered region',
);
});
+8 -5
View File
@@ -240,17 +240,20 @@ export function createQueries({ state, parts }) {
const health = aggregateTransitFeedHealth(statuses, now);
const count = state._vehicles.size;
if (state._enabled && active.length === 0) {
// English data (the analyst's empty note to the model, and the Layers
// row's prompt); both show it through translateStatusMessage
// (ui/statusText.js).
const note = state._altitudeGateOpen
? `No feed here yet · ${TRANSIT_ENABLED_FEEDS.length} regions available`
: `Fly below ${Math.round(ACTIVATION_ALTITUDE_M / 1000).toLocaleString('en-US')} km to a covered region`;
return {
count: 0,
lastUpdate: state._lastUpdate,
error: null,
source: 'GTFS-RT',
status: 'zoom-in',
// English data (the analyst's empty note to the model); the voice
// card shows it through NOTE_PATTERNS (voice/resultDisplay.js).
coverage: state._altitudeGateOpen
? `No feed here yet · ${TRANSIT_ENABLED_FEEDS.length} regions available`
: `Fly below ${Math.round(ACTIVATION_ALTITUDE_M / 1000).toLocaleString('en-US')} km to a covered region`,
statusMessage: note,
coverage: note,
};
}
// Age is shown by the same rule the row state uses, so the text and the
+3
View File
@@ -108,6 +108,9 @@ export function createViewport({ state, services, parts }) {
parts.ingestion.removeFeedVehicles(feedId);
}
}
// Leaving coverage leaves nothing on the map to date: a row that read
// "26s ago" over an empty view would claim a feed that is no longer here.
if (desired.size === 0) state._lastUpdate = null;
for (const [feedId, feed] of desired) {
if (!state._activeFeeds.has(feedId)) {
state._activeFeeds.set(feedId, feed);
+6 -3
View File
@@ -11,14 +11,17 @@ import {
} from './layerNames.js';
export { localizeLayerName } from './layerNames.js';
import { createWeatherPanel } from './weatherPanel.js';
import { translateFeedError, translateStatusLine } from './statusText.js';
import {
translateFeedError,
translateStatusLine,
translateStatusMessage,
} from './statusText.js';
import {
formatNumber,
getLocale,
msg,
onLocaleChange,
t,
translateKnown,
} from '../i18n/index.js';
// Feed-state values are machine ids (QA hooks, CSS feed-* classes); only the
@@ -691,7 +694,7 @@ export class LayerPanel {
typeof stats.statusMessage === 'string' &&
stats.statusMessage.trim()
) {
return `${source} · ${translateKnown(stats.statusMessage.trim())}`;
return `${source} · ${translateStatusMessage(stats.statusMessage.trim())}`;
}
const ago = stats.lastUpdate ? this._timeAgo(stats.lastUpdate) : t('never');
if (stats.loading) {
+54
View File
@@ -431,3 +431,57 @@ test('without the Utilities list, Directions stays in Data Layers under Utilitie
restore();
}
});
test('a layer with no feed in view says why instead of "never"', async () => {
const { LayerPanel, presentLayerCount, presentLayerToggle } =
await import('./layerPanel.js');
const { setLocale } = await import('../i18n/index.js');
const panel = LayerPanel.prototype;
const rows = [
{
id: 'transit',
name: 'Transit',
source: 'GTFS-RT',
enabled: true,
stats: {
count: 0,
lastUpdate: null,
source: 'GTFS-RT',
status: 'zoom-in',
statusMessage: 'No feed here yet · 16 regions available',
},
meta: 'GTFS-RT · No feed here yet · 16 regions available',
},
{
id: 'bikeshare',
name: 'Bikeshare',
source: 'GBFS',
enabled: true,
stats: {
count: 0,
lastUpdate: null,
loading: false,
status: 'zoom-in',
statusMessage: 'Fly below 48 km to a covered city',
},
meta: 'GBFS · Fly below 48 km to a covered city',
},
];
for (const layer of rows) {
// Guidance is normal operation: the layer is on and nothing has failed.
assert.equal(presentLayerToggle(layer).text, 'ON');
assert.equal(presentLayerCount(layer.stats).empty, true);
assert.equal(panel._buildMetaText(layer), layer.meta);
}
setLocale('ru');
try {
for (const layer of rows) {
const meta = panel._buildMetaText(layer);
assert.doesNotMatch(meta, /never|Fly below|No feed|covered/, meta);
assert.match(meta, /^(GTFS-RT|GBFS) · /, meta);
}
} finally {
setLocale('en');
}
});
+63
View File
@@ -265,3 +265,66 @@ export function translateStatusLine(text) {
.map(translateStatusPiece)
.join(STATUS_SEPARATOR);
}
/** Thousands separators out of an English number ("3,000" → 3000). */
const englishNumber = (text) => Number(text.replace(/,/g, ''));
// Coverage notes of a layer whose view has no feed, whole lines with a number
// in them: transit (transit/queries.js) and bikeshare (bikeshare/controls.js).
// Their " · " is part of one sentence, so they are matched before the line is
// split into pieces. Keep these in step with the producers.
const COVERAGE_NOTE_PATTERNS = [
[
/^Fly below ([\d,]+) km to a covered region$/,
([, km]) =>
t('Fly below {km} km to a covered region', {
km: formatNumber(englishNumber(km)),
}),
],
[
/^No feed here yet · ([\d,]+) regions available$/,
([, regions]) => {
const count = englishNumber(regions);
return tn(
count,
'No feed here yet · {n} region available',
'No feed here yet · {n} regions available',
{ n: formatNumber(count) },
);
},
],
[
/^Fly below ([\d,]+) km to a covered city$/,
([, km]) =>
t('Fly below {km} km to a covered city', {
km: formatNumber(englishNumber(km)),
}),
],
[
/^No bike share here yet · ([\d,]+) systems available$/,
([, systems]) => {
const count = englishNumber(systems);
return tn(
count,
'No bike share here yet · {n} system available',
'No bike share here yet · {n} systems available',
{ n: formatNumber(count) },
);
},
],
];
/**
* A layer's status message in the UI language: its catalog entry, a coverage
* note by its shape, or a status line translated piece by piece.
*/
export function translateStatusMessage(text) {
if (typeof text !== 'string') return text;
const known = translateKnown(text);
if (known !== text) return known;
for (const [pattern, format] of COVERAGE_NOTE_PATTERNS) {
const match = pattern.exec(text);
if (match) return format(match);
}
return translateStatusLine(text);
}
+57 -1
View File
@@ -3,7 +3,11 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { setLocale, t } from '../i18n/index.js';
import { translateFeedError, translateStatusLine } from './statusText.js';
import {
translateFeedError,
translateStatusLine,
translateStatusMessage,
} from './statusText.js';
function inLocale(locale, run) {
setLocale(locale);
@@ -75,3 +79,55 @@ for (const locale of ['es', 'ru']) {
});
});
}
// A layer's coverage note when no feed covers the view: one sentence with a
// " · " in it (transit/queries.js, bikeshare/controls.js).
const COVERAGE_NOTES = [
'Fly below 3,000 km to a covered region',
'No feed here yet · 16 regions available',
'Fly below 48 km to a covered city',
'No bike share here yet · 61 systems available',
];
test('en: coverage notes show as they came', () => {
setLocale('en');
for (const text of COVERAGE_NOTES)
assert.equal(translateStatusMessage(text), text);
assert.equal(
translateStatusMessage('No bike share here yet · 1 systems available'),
'No bike share here yet · 1 system available',
);
});
for (const locale of ['es', 'ru', 'ja']) {
test(`${locale}: a coverage note translates whole, numbers kept`, () => {
inLocale(locale, () => {
for (const text of COVERAGE_NOTES) {
const shown = translateStatusMessage(text);
assert.doesNotMatch(
shown,
/\b(Fly|below|covered|feed|bike share|available)\b/,
shown,
);
const number = /[\d,]+(?= km| regions| systems)/.exec(text)[0];
assert.ok(
shown.replace(/[^\d]/g, '').includes(number.replace(/,/g, '')),
shown,
);
}
});
});
test(`${locale}: a status message without a coverage note keeps its other forms`, () => {
inLocale(locale, () => {
assert.equal(
translateStatusMessage('Upstream response too large'),
t('Upstream response too large'),
);
assert.equal(
translateStatusMessage('2 feeds live · 14 heard'),
translateStatusLine('2 feeds live · 14 heard'),
);
});
});
}
+5 -26
View File
@@ -29,7 +29,7 @@ import {
} from '../i18n/index.js';
import { layerNameForId, sourceLabel } from '../ui/layerNames.js';
import { localizePlaceName } from '../ui/placeNames.js';
import { translateStatusLine } from '../ui/statusText.js';
import { translateStatusMessage } from '../ui/statusText.js';
import { formatQuantity } from '../ui/unitLabels.js';
/** Split "a; b" caveat text into separate notes. */
@@ -397,27 +397,6 @@ const NOTE_PATTERNS = [
'Contacts panel snapshot — the same retained cohort supplies the card, speech, and follow-up memory',
)) + period,
],
// Transit's empty note when no feed covers the view (transit/queries.js);
// the model gets the English.
[
/^Fly below ([\d,]+) km to a covered region$/,
([, km]) =>
t('Fly below {km} km to a covered region', {
km: formatNumber(Number(km.replace(/,/g, ''))),
}),
],
[
/^No feed here yet · ([\d,]+) regions available$/,
([, regions]) => {
const count = Number(regions.replace(/,/g, ''));
return tn(
count,
'No feed here yet · {n} region available',
'No feed here yet · {n} regions available',
{ n: formatNumber(count) },
);
},
],
[
/^([a-z0-9-]+) (off|loading|error)$/,
([, key, status]) =>
@@ -444,10 +423,10 @@ function shownNote(text) {
const shown = match ? show(match) : null;
if (shown) return shown;
}
// A layer's empty note can be its status line, English pieces joined by
// " · " ("feeds 1090, 978 stale · 0 heard", "TTC 0 (95s old)").
const known = translateKnown(text);
return known !== text ? known : translateStatusLine(text);
// A layer's empty note can be its coverage note ("Fly below 3,000 km to a
// covered region") or its status line, English pieces joined by " · "
// ("feeds 1090, 978 stale · 0 heard", "TTC 0 (95s old)").
return translateStatusMessage(text);
}
/** Feed states as the card's chips show them; the values stay English. */