mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix(layers): report Directions errors as English data
The Directions row errors (pointer blocked, no route, routing timed out,
no response from the routing service) and the route request failures
("Routing failed: {error}", "{error} — try again in a moment",
"Routing unavailable (HTTP {status})", the rate-limit and fallback
texts) were translated inside getStats(), so the layer snapshot that
voice tools and the analyst read carried Spanish or Russian text.
Fixed texts are marked with msg() and the interpolated ones are built
in English. The Layers panel translates them where it shows them,
through a ROUTING_ERROR_PATTERNS block in statusText.js that the
proxy's refusal texts move into. English reads as before. In other
locales two things differ slightly: a proxy text inside "Routing
failed: X" that is not one of the known refusals is now looked up in
the catalog instead of shown as given, and the "— try again in a
moment" shape is recognised on every layer's error, not only on
Directions.
This commit is contained in:
1 parent
93ea088f45
commit
4d2a71c1b2
4 files changed
+194
-44
No files matched your search
@@ -38,33 +38,21 @@ export const DIRECTIONS_MODES = Object.freeze({
|
||||
export const DEFAULT_DIRECTIONS_MODE = 'car';
|
||||
|
||||
/**
|
||||
* The route proxy's refusal texts (server/providers/places/routes.js). They
|
||||
* stay English on the wire and in the AI tools; the row shows them translated.
|
||||
* The row error for a failed route request (createDirectionsSource errors),
|
||||
* as English data; the Layers panel shows it translated (ROUTING_ERROR_PATTERNS
|
||||
* in src/ui/statusText.js).
|
||||
*/
|
||||
const ROUTING_REFUSALS = new Set([
|
||||
msg('rate limited'),
|
||||
msg('routing service is rate limited'),
|
||||
msg('route leg too long'),
|
||||
msg('route too long'),
|
||||
]);
|
||||
|
||||
/** A refusal text from the route proxy in the language; unknown ones as given. */
|
||||
export function presentRoutingRefusal(text) {
|
||||
return ROUTING_REFUSALS.has(text) ? t(text) : text;
|
||||
}
|
||||
|
||||
/** The row text for a failed route request (createDirectionsSource errors). */
|
||||
export function presentRoutingFailure(error) {
|
||||
if (error?.status === 429) {
|
||||
return error.refusal
|
||||
? t('{error} — try again in a moment', {
|
||||
error: presentRoutingRefusal(error.refusal),
|
||||
})
|
||||
: t('Routing is rate limited — try again in a moment');
|
||||
? // i18n-ignore: English data, shown through ROUTING_ERROR_PATTERNS
|
||||
`${error.refusal} — try again in a moment`
|
||||
: msg('Routing is rate limited — try again in a moment');
|
||||
}
|
||||
if (Number.isFinite(error?.status) && error.status > 0)
|
||||
return t('Routing unavailable (HTTP {status})', { status: error.status });
|
||||
return error?.message || t('Routing unavailable');
|
||||
// i18n-ignore: English data, shown through ROUTING_ERROR_PATTERNS
|
||||
return `Routing unavailable (HTTP ${error.status})`;
|
||||
return error?.message || msg('Routing unavailable');
|
||||
}
|
||||
|
||||
/** Route colour — the annotation palette's cyan, so voice routes and Directions match. */
|
||||
@@ -643,7 +631,9 @@ export function directionsStats(state) {
|
||||
return {
|
||||
count: route?.steps.length || 0,
|
||||
lastUpdate,
|
||||
error: t('Another map tool is using clicks — close it, then SET A again'),
|
||||
error: msg(
|
||||
'Another map tool is using clicks — close it, then SET A again',
|
||||
),
|
||||
status: 'empty',
|
||||
source,
|
||||
};
|
||||
@@ -662,7 +652,7 @@ export function directionsStats(state) {
|
||||
return {
|
||||
count: 0,
|
||||
lastUpdate,
|
||||
error: error || t('No route found'),
|
||||
error: error || msg('No route found'),
|
||||
status: 'empty',
|
||||
source,
|
||||
};
|
||||
@@ -1144,13 +1134,10 @@ export function createDirectionsLayer({ services, source }) {
|
||||
clearRouteGraphics();
|
||||
_status = 'error';
|
||||
_error =
|
||||
payload?.error === 'no route found'
|
||||
? t('No route found between A and B')
|
||||
: payload?.error
|
||||
? t('Routing failed: {error}', {
|
||||
error: presentRoutingRefusal(payload.error),
|
||||
})
|
||||
: t('No route found between A and B');
|
||||
// i18n-ignore: English data, shown through ROUTING_ERROR_PATTERNS
|
||||
payload?.error && payload.error !== 'no route found'
|
||||
? `Routing failed: ${payload.error}`
|
||||
: msg('No route found between A and B');
|
||||
return;
|
||||
}
|
||||
_route = route;
|
||||
@@ -1167,9 +1154,9 @@ export function createDirectionsLayer({ services, source }) {
|
||||
// happened instead.
|
||||
_error =
|
||||
error?.name === 'AbortError'
|
||||
? t('Routing timed out')
|
||||
? msg('Routing timed out')
|
||||
: error?.name === 'TypeError'
|
||||
? t('Routing unavailable — no response from the routing service')
|
||||
? msg('Routing unavailable — no response from the routing service')
|
||||
: presentRoutingFailure(error);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// Directions reports its fixed errors as English data (getStats(), and so the
|
||||
// layer snapshot the voice tools and the analyst read); the Layers panel row
|
||||
// shows them in the UI language.
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { setLocale, t as translate } from '../../i18n/index.js';
|
||||
import { LayerPanel } from '../../ui/layerPanel.js';
|
||||
import { directionsStats, presentRoutingFailure } from './index.js';
|
||||
|
||||
const idle = {
|
||||
enabled: true,
|
||||
mode: 'car',
|
||||
armed: null,
|
||||
a: null,
|
||||
b: null,
|
||||
status: 'idle',
|
||||
error: null,
|
||||
route: null,
|
||||
lastUpdate: null,
|
||||
};
|
||||
|
||||
const cases = [
|
||||
[
|
||||
{ ...idle, pointerBlocked: true },
|
||||
'Another map tool is using clicks — close it, then SET A again',
|
||||
],
|
||||
[{ ...idle, status: 'error' }, 'No route found'],
|
||||
[
|
||||
{ ...idle, status: 'error', error: 'Routing timed out' },
|
||||
'Routing timed out',
|
||||
],
|
||||
];
|
||||
|
||||
function rowFor(stats) {
|
||||
return LayerPanel.prototype._buildMetaText({
|
||||
id: 'directions',
|
||||
enabled: true,
|
||||
stats,
|
||||
});
|
||||
}
|
||||
|
||||
// Route request failures with the proxy's refusal or an HTTP status in them:
|
||||
// [the request error, its English row error, the row text in the language]
|
||||
const failures = () => [
|
||||
[
|
||||
{ status: 429, refusal: 'route leg too long' },
|
||||
'route leg too long — try again in a moment',
|
||||
translate('{error} — try again in a moment', {
|
||||
error: translate('route leg too long'),
|
||||
}),
|
||||
],
|
||||
[
|
||||
{ status: 429, refusal: null },
|
||||
'Routing is rate limited — try again in a moment',
|
||||
translate('Routing is rate limited — try again in a moment'),
|
||||
],
|
||||
[
|
||||
{ status: 503 },
|
||||
'Routing unavailable (HTTP 503)',
|
||||
translate('Routing unavailable (HTTP {status})', { status: 503 }),
|
||||
],
|
||||
[
|
||||
{ status: 0, message: '' },
|
||||
'Routing unavailable',
|
||||
translate('Routing unavailable'),
|
||||
],
|
||||
];
|
||||
|
||||
for (const locale of ['es', 'ru']) {
|
||||
test(`${locale}: Directions errors stay English in stats and read translated in the row`, (t) => {
|
||||
t.after(() => setLocale('en'));
|
||||
setLocale(locale);
|
||||
for (const [state, english] of cases) {
|
||||
const stats = directionsStats(state);
|
||||
assert.equal(stats.error, english);
|
||||
const row = rowFor(stats);
|
||||
assert.ok(row.includes(translate(english)), `${locale}: ${row}`);
|
||||
assert.ok(!row.includes(english), `${locale}: ${row}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const locale of ['es', 'ru']) {
|
||||
test(`${locale}: route request failures stay English in stats and read translated in the row`, (t) => {
|
||||
t.after(() => setLocale('en'));
|
||||
setLocale(locale);
|
||||
for (const [failure, english, shown] of failures()) {
|
||||
const error = presentRoutingFailure(failure);
|
||||
assert.equal(error, english);
|
||||
const stats = directionsStats({ ...idle, status: 'error', error });
|
||||
assert.equal(stats.error, english);
|
||||
assert.ok(rowFor(stats).includes(shown), `${locale}: ${rowFor(stats)}`);
|
||||
assert.notEqual(shown, english);
|
||||
}
|
||||
// The proxy's refusal after "Routing failed:", and a text it never sends.
|
||||
for (const [refusal, shownRefusal] of [
|
||||
['route too long', translate('route too long')],
|
||||
['something else', 'something else'],
|
||||
]) {
|
||||
const row = rowFor(
|
||||
directionsStats({
|
||||
...idle,
|
||||
status: 'error',
|
||||
error: `Routing failed: ${refusal}`,
|
||||
}),
|
||||
);
|
||||
assert.ok(
|
||||
row.includes(
|
||||
translate('Routing failed: {error}', { error: shownRefusal }),
|
||||
),
|
||||
`${locale}: ${row}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('en: the Directions row shows the English error', () => {
|
||||
setLocale('en');
|
||||
const row = rowFor(directionsStats({ ...idle, status: 'error' }));
|
||||
assert.match(row, /No route found/);
|
||||
});
|
||||
+33
-1
@@ -112,12 +112,44 @@ const FEED_ERROR_PATTERNS = [
|
||||
],
|
||||
];
|
||||
|
||||
// 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'),
|
||||
]);
|
||||
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 FEED_ERROR_PATTERNS) {
|
||||
for (const [pattern, format] of ERROR_PATTERNS) {
|
||||
const match = pattern.exec(text);
|
||||
if (match) return format(match);
|
||||
}
|
||||
|
||||
@@ -7,10 +7,8 @@ import { setLocale, t, translateKnown } from '../i18n/index.js';
|
||||
import { presentStreetLevelPanel } from './streetLevelPresentation.js';
|
||||
import { providerSnapshot } from '../testSupport/streetLevelFakes.mjs';
|
||||
import { createWeatherSource } from '../layers/weather/source.js';
|
||||
import {
|
||||
presentRoutingFailure,
|
||||
presentRoutingRefusal,
|
||||
} from '../layers/directions/index.js';
|
||||
import { presentRoutingFailure } from '../layers/directions/index.js';
|
||||
import { translateFeedError } from './statusText.js';
|
||||
import { AIM_AT_GROUND_MESSAGE } from '../layers/streetLevel/policy.js';
|
||||
|
||||
const NO_IMAGERY = 'No street-level imagery within 50 m of that point';
|
||||
@@ -53,9 +51,13 @@ for (const locale of ['es', 'ru']) {
|
||||
test(`${locale}: the route proxy's refusals are shown translated`, () => {
|
||||
setLocale(locale);
|
||||
try {
|
||||
const shown = t('Routing failed: {error}', {
|
||||
error: presentRoutingRefusal('route leg too long'),
|
||||
});
|
||||
// Directions stores these in English; the Layers panel shows them
|
||||
// through translateFeedError.
|
||||
const shown = translateFeedError('Routing failed: route leg too long');
|
||||
assert.equal(
|
||||
shown,
|
||||
t('Routing failed: {error}', { error: t('route leg too long') }),
|
||||
);
|
||||
assert.doesNotMatch(shown, /route leg too long/);
|
||||
const limited = presentRoutingFailure(
|
||||
Object.assign(new Error('rate limited — try again in a moment'), {
|
||||
@@ -63,16 +65,24 @@ for (const locale of ['es', 'ru']) {
|
||||
refusal: 'rate limited',
|
||||
}),
|
||||
);
|
||||
assert.doesNotMatch(limited, /rate limited|try again in a moment/);
|
||||
assert.equal(limited, 'rate limited — try again in a moment');
|
||||
assert.doesNotMatch(
|
||||
translateFeedError(limited),
|
||||
/rate limited|try again in a moment/,
|
||||
);
|
||||
const http = presentRoutingFailure(
|
||||
Object.assign(new Error('Routing unavailable (HTTP 503)'), {
|
||||
status: 503,
|
||||
}),
|
||||
);
|
||||
assert.notEqual(http, 'Routing unavailable (HTTP 503)');
|
||||
assert.match(http, /503/);
|
||||
assert.equal(http, 'Routing unavailable (HTTP 503)');
|
||||
assert.notEqual(translateFeedError(http), http);
|
||||
assert.match(translateFeedError(http), /503/);
|
||||
// A text the proxy never sends is shown as given.
|
||||
assert.equal(presentRoutingRefusal('something else'), 'something else');
|
||||
assert.equal(
|
||||
translateFeedError('Routing failed: something else'),
|
||||
t('Routing failed: {error}', { error: 'something else' }),
|
||||
);
|
||||
} finally {
|
||||
setLocale('en');
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user