mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25c33893f6 | ||
|
|
b6ad32009f | ||
|
|
7d8b80db0a | ||
|
|
e84cae6baa | ||
|
|
06c84f92d3 |
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
- Chat autocomplete finds Arabic suggestions typed without short vowels or
|
||||||
|
shadda, so «تتبع» now suggests «اعرض تتبّع الرحلات المباشر»
|
||||||
|
(German, @gekh, #1561).
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- Say why Transit and Bike Share are empty outside coverage. Over a city with
|
||||||
|
no feed or system (London, Tokyo) their Layers rows read
|
||||||
|
`No feed here yet · 16 regions available` and
|
||||||
|
`No bike share here yet · 61 systems available`, or how low to fly, instead
|
||||||
|
of `never`; leaving coverage clears the last-update time (German, @gekh,
|
||||||
|
#1576).
|
||||||
@@ -1817,6 +1817,19 @@ Mapped-installation zoom guidance appears in the layer row without counting as
|
|||||||
a global loading failure. Guidance statuses do not suppress independent refresh
|
a global loading failure. Guidance statuses do not suppress independent refresh
|
||||||
errors.
|
errors.
|
||||||
|
|
||||||
|
Transit and Bike Share report the same kind of guidance when no feed or system
|
||||||
|
covers the view, instead of an undated `never`. Above their altitude gates the
|
||||||
|
rows read `GTFS-RT · Fly below 3,000 km to a covered region` and
|
||||||
|
`GBFS · Fly below 48 km to a covered city`; lower down over a city neither
|
||||||
|
covers (London, Tokyo) they read `GTFS-RT · No feed here yet · N regions
|
||||||
|
available` and `GBFS · No bike share here yet · N systems available`, N being
|
||||||
|
the enabled feeds and the catalogued systems. The
|
||||||
|
chip stays ON. A Bike Share city that is loading or failed keeps its own
|
||||||
|
loading or error text. Leaving every covered feed or system clears the layer's
|
||||||
|
last-update time, so the row never dates data it no longer draws. The same
|
||||||
|
English notes are the voice analyst's empty-result notes; both surfaces show
|
||||||
|
them translated through `translateStatusMessage` (`src/ui/statusText.js`).
|
||||||
|
|
||||||
## Places and CCTV request bounds
|
## Places and CCTV request bounds
|
||||||
|
|
||||||
With a Google key configured, nearby and text search reject missing, blank,
|
With a Google key configured, nearby and text search reject missing, blank,
|
||||||
|
|||||||
@@ -2912,6 +2912,15 @@
|
|||||||
"many": "لا مصدر بيانات هنا بعد · {n} منطقة متاحة",
|
"many": "لا مصدر بيانات هنا بعد · {n} منطقة متاحة",
|
||||||
"other": "لا مصدر بيانات هنا بعد · {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": "متدهور",
|
"degraded": "متدهور",
|
||||||
"stale": "متقادم",
|
"stale": "متقادم",
|
||||||
"partial": "جزئي",
|
"partial": "جزئي",
|
||||||
|
|||||||
@@ -2662,6 +2662,11 @@
|
|||||||
"one": "Hier noch kein Feed · {n} Region verfügbar",
|
"one": "Hier noch kein Feed · {n} Region verfügbar",
|
||||||
"other": "Hier noch kein Feed · {n} Regionen 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",
|
"degraded": "eingeschränkt",
|
||||||
"stale": "veraltet",
|
"stale": "veraltet",
|
||||||
"partial": "teilweise",
|
"partial": "teilweise",
|
||||||
|
|||||||
@@ -2821,6 +2821,12 @@
|
|||||||
"many": "Aún no hay fuente aquí · {n} de regiones disponibles",
|
"many": "Aún no hay fuente aquí · {n} de regiones disponibles",
|
||||||
"other": "Aún no hay fuente aquí · {n} 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",
|
"Wind speed field unavailable": "Campo de velocidad del viento no disponible",
|
||||||
"Decoder feeds": "Fuentes del decodificador",
|
"Decoder feeds": "Fuentes del decodificador",
|
||||||
"WebUSB + decoder feeds": "WebUSB + fuentes del decodificador",
|
"WebUSB + decoder feeds": "WebUSB + fuentes del decodificador",
|
||||||
|
|||||||
@@ -2531,6 +2531,12 @@
|
|||||||
"many": "Pas encore de flux ici · {n} de régions disponibles",
|
"many": "Pas encore de flux ici · {n} de régions disponibles",
|
||||||
"other": "Pas encore de flux ici · {n} 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é",
|
"degraded": "dégradé",
|
||||||
"stale": "obsolète",
|
"stale": "obsolète",
|
||||||
"partial": "partiel",
|
"partial": "partiel",
|
||||||
|
|||||||
@@ -2531,6 +2531,12 @@
|
|||||||
"many": "Ancora nessun feed qui · {n} di regioni disponibili",
|
"many": "Ancora nessun feed qui · {n} di regioni disponibili",
|
||||||
"other": "Ancora nessun feed qui · {n} 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",
|
"degraded": "degradato",
|
||||||
"stale": "obsoleto",
|
"stale": "obsoleto",
|
||||||
"partial": "parziale",
|
"partial": "parziale",
|
||||||
|
|||||||
@@ -2401,6 +2401,10 @@
|
|||||||
"No feed here yet · {n} regions available": {
|
"No feed here yet · {n} regions available": {
|
||||||
"other": "ここにはまだフィードがありません · 利用可能な地域 {n}か所"
|
"other": "ここにはまだフィードがありません · 利用可能な地域 {n}か所"
|
||||||
},
|
},
|
||||||
|
"Fly below {km} km to a covered city": "{km} km未満まで高度を下げ、カバー範囲内の都市へ移動してください",
|
||||||
|
"No bike share here yet · {n} systems available": {
|
||||||
|
"other": "ここにはまだシェアサイクルがありません · 利用可能なシステム {n}件"
|
||||||
|
},
|
||||||
"degraded": "性能低下",
|
"degraded": "性能低下",
|
||||||
"stale": "更新遅延",
|
"stale": "更新遅延",
|
||||||
"partial": "部分的",
|
"partial": "部分的",
|
||||||
|
|||||||
@@ -2533,6 +2533,12 @@
|
|||||||
"many": "Ainda não há feed aqui · {n} de regiões disponíveis",
|
"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"
|
"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",
|
"degraded": "degradado",
|
||||||
"stale": "desatualizado",
|
"stale": "desatualizado",
|
||||||
"partial": "parcial",
|
"partial": "parcial",
|
||||||
|
|||||||
@@ -2884,6 +2884,13 @@
|
|||||||
"many": "Здесь пока нет потока данных · доступно {n} регионов",
|
"many": "Здесь пока нет потока данных · доступно {n} регионов",
|
||||||
"other": "Здесь пока нет потока данных · доступно {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": "Поле скорости ветра недоступно",
|
"Wind speed field unavailable": "Поле скорости ветра недоступно",
|
||||||
"Decoder feeds": "Потоки декодера",
|
"Decoder feeds": "Потоки декодера",
|
||||||
"WebUSB + decoder feeds": "WebUSB + потоки декодера",
|
"WebUSB + decoder feeds": "WebUSB + потоки декодера",
|
||||||
|
|||||||
@@ -2595,6 +2595,13 @@
|
|||||||
"many": "Тут поки немає потоку даних · доступно {n} регіонів",
|
"many": "Тут поки немає потоку даних · доступно {n} регіонів",
|
||||||
"other": "Тут поки немає потоку даних · доступно {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": "з перебоями",
|
"degraded": "з перебоями",
|
||||||
"stale": "застарів",
|
"stale": "застарів",
|
||||||
"partial": "частково",
|
"partial": "частково",
|
||||||
|
|||||||
@@ -2302,6 +2302,10 @@
|
|||||||
"No feed here yet · {n} regions available": {
|
"No feed here yet · {n} regions available": {
|
||||||
"other": "此处暂无数据源 · 有 {n} 个覆盖区域"
|
"other": "此处暂无数据源 · 有 {n} 个覆盖区域"
|
||||||
},
|
},
|
||||||
|
"Fly below {km} km to a covered city": "请降至 {km} km 以下并飞往有覆盖的城市",
|
||||||
|
"No bike share here yet · {n} systems available": {
|
||||||
|
"other": "此处暂无共享单车 · 有 {n} 个可用系统"
|
||||||
|
},
|
||||||
"degraded": "降级",
|
"degraded": "降级",
|
||||||
"stale": "过期",
|
"stale": "过期",
|
||||||
"partial": "部分可用",
|
"partial": "部分可用",
|
||||||
|
|||||||
@@ -2290,6 +2290,10 @@
|
|||||||
"No feed here yet · {n} regions available": {
|
"No feed here yet · {n} regions available": {
|
||||||
"other": "此處暫無數據源 · 有 {n} 個覆蓋區域"
|
"other": "此處暫無數據源 · 有 {n} 個覆蓋區域"
|
||||||
},
|
},
|
||||||
|
"Fly below {km} km to a covered city": "請降至 {km} km 以下並飛往有覆蓋的城市",
|
||||||
|
"No bike share here yet · {n} systems available": {
|
||||||
|
"other": "此處暫無共享單車 · 有 {n} 個可用系統"
|
||||||
|
},
|
||||||
"degraded": "降級",
|
"degraded": "降級",
|
||||||
"stale": "過期",
|
"stale": "過期",
|
||||||
"partial": "部分可用",
|
"partial": "部分可用",
|
||||||
|
|||||||
Vendored
+20
-2
@@ -1,5 +1,6 @@
|
|||||||
import { msg, t } from '../../i18n/index.js';
|
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 }) {
|
export function createControls({ state: layerState, services, parts, source }) {
|
||||||
const methods = {
|
const methods = {
|
||||||
@@ -57,7 +58,7 @@ export function createControls({ state: layerState, services, parts, source }) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return current layer statistics for the UI status display.
|
* 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() {
|
getStats() {
|
||||||
const stats = {
|
const stats = {
|
||||||
@@ -65,6 +66,23 @@ export function createControls({ state: layerState, services, parts, source }) {
|
|||||||
lastUpdate: layerState._lastUpdate,
|
lastUpdate: layerState._lastUpdate,
|
||||||
loading: layerState._loading,
|
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) {
|
if (layerState._loading) {
|
||||||
stats.loadingLabel =
|
stats.loadingLabel =
|
||||||
layerState._activeCityIds.size > 0
|
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`);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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.
|
// A failure belongs to a city in range; none are once everything is off.
|
||||||
layerState._failedCityIds.clear();
|
layerState._failedCityIds.clear();
|
||||||
layerState._count = 0;
|
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;
|
layerState._activeCityIds = nextActive;
|
||||||
if (layerState._activeCityIds.size === 0) {
|
if (layerState._activeCityIds.size === 0) {
|
||||||
layerState._count = 0;
|
layerState._count = 0;
|
||||||
|
layerState._lastUpdate = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3982,3 +3982,42 @@ test('parked transit reprojects held and reported courses at the bounded camera
|
|||||||
'selected unknown course also stays screen-up',
|
'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',
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -240,17 +240,20 @@ export function createQueries({ state, parts }) {
|
|||||||
const health = aggregateTransitFeedHealth(statuses, now);
|
const health = aggregateTransitFeedHealth(statuses, now);
|
||||||
const count = state._vehicles.size;
|
const count = state._vehicles.size;
|
||||||
if (state._enabled && active.length === 0) {
|
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 {
|
return {
|
||||||
count: 0,
|
count: 0,
|
||||||
lastUpdate: state._lastUpdate,
|
lastUpdate: state._lastUpdate,
|
||||||
error: null,
|
error: null,
|
||||||
source: 'GTFS-RT',
|
source: 'GTFS-RT',
|
||||||
status: 'zoom-in',
|
status: 'zoom-in',
|
||||||
// English data (the analyst's empty note to the model); the voice
|
statusMessage: note,
|
||||||
// card shows it through NOTE_PATTERNS (voice/resultDisplay.js).
|
coverage: note,
|
||||||
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`,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Age is shown by the same rule the row state uses, so the text and the
|
// Age is shown by the same rule the row state uses, so the text and the
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ export function createViewport({ state, services, parts }) {
|
|||||||
parts.ingestion.removeFeedVehicles(feedId);
|
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) {
|
for (const [feedId, feed] of desired) {
|
||||||
if (!state._activeFeeds.has(feedId)) {
|
if (!state._activeFeeds.has(feedId)) {
|
||||||
state._activeFeeds.set(feedId, feed);
|
state._activeFeeds.set(feedId, feed);
|
||||||
|
|||||||
@@ -11,14 +11,17 @@ import {
|
|||||||
} from './layerNames.js';
|
} from './layerNames.js';
|
||||||
export { localizeLayerName } from './layerNames.js';
|
export { localizeLayerName } from './layerNames.js';
|
||||||
import { createWeatherPanel } from './weatherPanel.js';
|
import { createWeatherPanel } from './weatherPanel.js';
|
||||||
import { translateFeedError, translateStatusLine } from './statusText.js';
|
import {
|
||||||
|
translateFeedError,
|
||||||
|
translateStatusLine,
|
||||||
|
translateStatusMessage,
|
||||||
|
} from './statusText.js';
|
||||||
import {
|
import {
|
||||||
formatNumber,
|
formatNumber,
|
||||||
getLocale,
|
getLocale,
|
||||||
msg,
|
msg,
|
||||||
onLocaleChange,
|
onLocaleChange,
|
||||||
t,
|
t,
|
||||||
translateKnown,
|
|
||||||
} from '../i18n/index.js';
|
} from '../i18n/index.js';
|
||||||
|
|
||||||
// Feed-state values are machine ids (QA hooks, CSS feed-* classes); only the
|
// Feed-state values are machine ids (QA hooks, CSS feed-* classes); only the
|
||||||
@@ -691,7 +694,7 @@ export class LayerPanel {
|
|||||||
typeof stats.statusMessage === 'string' &&
|
typeof stats.statusMessage === 'string' &&
|
||||||
stats.statusMessage.trim()
|
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');
|
const ago = stats.lastUpdate ? this._timeAgo(stats.lastUpdate) : t('never');
|
||||||
if (stats.loading) {
|
if (stats.loading) {
|
||||||
|
|||||||
@@ -431,3 +431,57 @@ test('without the Utilities list, Directions stays in Data Layers under Utilitie
|
|||||||
restore();
|
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -265,3 +265,66 @@ export function translateStatusLine(text) {
|
|||||||
.map(translateStatusPiece)
|
.map(translateStatusPiece)
|
||||||
.join(STATUS_SEPARATOR);
|
.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);
|
||||||
|
}
|
||||||
@@ -3,7 +3,11 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { setLocale, t } from '../i18n/index.js';
|
import { setLocale, t } from '../i18n/index.js';
|
||||||
import { translateFeedError, translateStatusLine } from './statusText.js';
|
import {
|
||||||
|
translateFeedError,
|
||||||
|
translateStatusLine,
|
||||||
|
translateStatusMessage,
|
||||||
|
} from './statusText.js';
|
||||||
|
|
||||||
function inLocale(locale, run) {
|
function inLocale(locale, run) {
|
||||||
setLocale(locale);
|
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'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -22,6 +22,10 @@ export function bindChatPreferences({ ui, getDraft, setDraft }) {
|
|||||||
msg('Reset to globe view'),
|
msg('Reset to globe view'),
|
||||||
msg('Show earthquakes'),
|
msg('Show earthquakes'),
|
||||||
].map((text) => t(text));
|
].map((text) => t(text));
|
||||||
|
// Arabic short vowels and shadda are optional in writing: typing "تتبع"
|
||||||
|
// still finds "تتبّع".
|
||||||
|
const fold = (text) =>
|
||||||
|
text.toLowerCase().replace(/[\u064B-\u065F\u0670]/g, '');
|
||||||
try {
|
try {
|
||||||
const stored = JSON.parse(localStorage.getItem(key));
|
const stored = JSON.parse(localStorage.getItem(key));
|
||||||
if (typeof stored?.quick === 'boolean') quick = stored.quick;
|
if (typeof stored?.quick === 'boolean') quick = stored.quick;
|
||||||
@@ -31,18 +35,14 @@ export function bindChatPreferences({ ui, getDraft, setDraft }) {
|
|||||||
function render() {
|
function render() {
|
||||||
const draft = getDraft();
|
const draft = getDraft();
|
||||||
if (historyIndex >= 0 && draft !== recent[historyIndex]) resetHistory();
|
if (historyIndex >= 0 && draft !== recent[historyIndex]) resetHistory();
|
||||||
const query = draft.trim().toLowerCase();
|
const query = fold(draft.trim());
|
||||||
const groups = [];
|
const groups = [];
|
||||||
if (query) {
|
if (query) {
|
||||||
const candidates = [...recent, ...(quick ? quickCommands() : [])];
|
const candidates = [...recent, ...(quick ? quickCommands() : [])];
|
||||||
groups.push({
|
groups.push({
|
||||||
label: t('Suggestions'),
|
label: t('Suggestions'),
|
||||||
items: [...new Set(candidates)]
|
items: [...new Set(candidates)]
|
||||||
.filter(
|
.filter((text) => fold(text).includes(query) && fold(text) !== query)
|
||||||
(text) =>
|
|
||||||
text.toLowerCase().includes(query) &&
|
|
||||||
text.toLowerCase() !== query,
|
|
||||||
)
|
|
||||||
.slice(0, 4),
|
.slice(0, 4),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
|
import { setLocale } from '../i18n/index.js';
|
||||||
import { bindChatPreferences } from './chatPreferences.js';
|
import { bindChatPreferences } from './chatPreferences.js';
|
||||||
import { createTestVoiceUi } from '../testSupport/voiceUi.mjs';
|
import { createTestVoiceUi } from '../testSupport/voiceUi.mjs';
|
||||||
|
|
||||||
@@ -99,3 +100,21 @@ test('editing a recalled message ends recall, and sending resets it', (t) => {
|
|||||||
press('ArrowUp');
|
press('ArrowUp');
|
||||||
assert.equal(field.value, 'Show live flights now');
|
assert.equal(field.value, 'Show live flights now');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('autocomplete ignores Arabic short vowels and shadda', (t) => {
|
||||||
|
setLocale('ar');
|
||||||
|
t.after(() => setLocale('en'));
|
||||||
|
const ui = createTestVoiceUi();
|
||||||
|
let draft = '';
|
||||||
|
const prefs = bindChatPreferences({ ui, getDraft: () => draft });
|
||||||
|
t.after(() => prefs.destroy());
|
||||||
|
const suggested = (text) => {
|
||||||
|
draft = text;
|
||||||
|
prefs.render();
|
||||||
|
return ui.state.chat.suggestions.flatMap((group) => group.items);
|
||||||
|
};
|
||||||
|
// The quick command is written "تتبّع", with a shadda.
|
||||||
|
assert.deepEqual(suggested('تتبع'), ['اعرض تتبّع الرحلات المباشر']);
|
||||||
|
prefs.remember('انتقل إلى طوكيو');
|
||||||
|
assert.deepEqual(suggested('طُوكيو'), ['انتقل إلى طوكيو']);
|
||||||
|
});
|
||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
} from '../i18n/index.js';
|
} from '../i18n/index.js';
|
||||||
import { layerNameForId, sourceLabel } from '../ui/layerNames.js';
|
import { layerNameForId, sourceLabel } from '../ui/layerNames.js';
|
||||||
import { localizePlaceName } from '../ui/placeNames.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';
|
import { formatQuantity } from '../ui/unitLabels.js';
|
||||||
|
|
||||||
/** Split "a; b" caveat text into separate notes. */
|
/** 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',
|
'Contacts panel snapshot — the same retained cohort supplies the card, speech, and follow-up memory',
|
||||||
)) + period,
|
)) + 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)$/,
|
/^([a-z0-9-]+) (off|loading|error)$/,
|
||||||
([, key, status]) =>
|
([, key, status]) =>
|
||||||
@@ -444,10 +423,10 @@ function shownNote(text) {
|
|||||||
const shown = match ? show(match) : null;
|
const shown = match ? show(match) : null;
|
||||||
if (shown) return shown;
|
if (shown) return shown;
|
||||||
}
|
}
|
||||||
// A layer's empty note can be its status line, English pieces joined by
|
// A layer's empty note can be its coverage note ("Fly below 3,000 km to a
|
||||||
// " · " ("feeds 1090, 978 stale · 0 heard", "TTC 0 (95s old)").
|
// covered region") or its status line, English pieces joined by " · "
|
||||||
const known = translateKnown(text);
|
// ("feeds 1090, 978 stale · 0 heard", "TTC 0 (95s old)").
|
||||||
return known !== text ? known : translateStatusLine(text);
|
return translateStatusMessage(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Feed states as the card's chips show them; the values stay English. */
|
/** Feed states as the card's chips show them; the values stay English. */
|
||||||
|
|||||||
Reference in new issue
Block a user