Compare commits

..
Author SHA1 Message Date
German Khokhlov d42b6fe764 Merge branch 'main' into german/map-render-error-state 2026-11-13 18:47:36 +04:00
German Khokhlov 29cf9ff8f6 Add the render failure notice in Swedish 2026-11-11 19:37:06 +04:00
German Khokhlov 897a74ea27 Merge remote-tracking branch 'origin/main' into german/map-render-error-state
# Conflicts:
#	src/i18n/locales/ar.json
#	src/i18n/locales/de.json
#	src/i18n/locales/es.json
#	src/i18n/locales/fr.json
#	src/i18n/locales/he.json
#	src/i18n/locales/it.json
#	src/i18n/locales/ja.json
#	src/i18n/locales/pt.json
#	src/i18n/locales/ru.json
#	src/i18n/locales/uk.json
#	src/i18n/locales/zh-CN.json
#	src/i18n/locales/zh-Hant.json
2026-11-11 19:33:49 +04:00
German Khokhlov bd1da596b8 Keep the draw tool test file in its own formatting 2026-11-11 19:25:02 +04:00
German Khokhlov 6976cbc941 Hand focus back to the globe canvas after a render failure
When the control that had focus is gone or cannot take it, focus went to
the #cesiumContainer div, where Enter no longer finished a drawing. It now
goes to the globe's canvas, which the map's keys read as the map; the canvas
is focusable only until focus moves on or the globe is pressed. A control
disabled while the notice was up no longer leaves focus on the page, and
neither focus call scrolls.
2026-11-11 18:03:30 +04:00
German Khokhlov 771ed98e3b Add the render failure notice in Hebrew 2026-11-11 12:48:44 +04:00
German Khokhlov 282ad6c5f6 Merge remote-tracking branch 'origin/main' into german/map-render-error-state
# Conflicts:
#	src/i18n/locales/ar.json
#	src/i18n/locales/de.json
#	src/i18n/locales/es.json
#	src/i18n/locales/fr.json
#	src/i18n/locales/it.json
#	src/i18n/locales/ja.json
#	src/i18n/locales/pt.json
#	src/i18n/locales/ru.json
#	src/i18n/locales/uk.json
#	src/i18n/locales/zh-CN.json
#	src/i18n/locales/zh-Hant.json
2026-11-11 12:45:05 +04:00
German Khokhlov d1f4d1acdc Say the map is not updated in the Arabic render failure notice 2026-11-11 12:44:08 +04:00
German Khokhlov ebaa439739 Tighten the render failure notice's error handling and focus
- An error a frame of the inline panel's timer throws no longer reaches the
  host page; it has been raised as a render error by then.
- An error Scene raised and threw on (rethrowRenderErrors) is raised once.
- After a lost context, later render errors are only logged, so the notice
  keeps saying why.
- A render error without a value shows no detail.
- The globe's tile load progress and the scene's tileset load events are
  finished too when a listener's error cuts them short.
- Focus goes back where it was when the globe draws again, or to the map.
- The appearance counter and the guard for a missing viewer are gone.

Tests cover every finished event, a thrown timer frame, a single report under
rethrowRenderErrors and the focus return; the inline tests share one setup.
2026-11-11 12:44:08 +04:00
German Khokhlov 265074aa54 Raise the render failure notice above floating panels
The notice sat at z-index 1001, under the Québec camera viewer (2000), which
covered its text and, with video playing, its buttons on a phone. It now sits
at 2001. On touch screens its buttons are 44 px tall.
2026-11-11 12:44:08 +04:00
German Khokhlov c763c38193 Keep the render loop marked running after a frame Retry could race
An error Scene catches itself marks Cesium's render loop stopped while the
loop's next animation frame is still queued. A Retry, or a tab shown again,
before that frame ran started a second loop, and the globe drew every frame
twice. The render and resize wrapper now keeps the loop marked running after
a call made while it ran; the queued frame stops it if nothing restarted it.
This also covers the inline panel's timer frames, so its own guard goes.
2026-11-10 16:40:22 +04:00
German Khokhlov 2448662574 Merge remote-tracking branch 'origin/main' into german/map-render-error-state
# Conflicts:
#	src/app/scene.js
#	src/app/viewer.js
#	src/i18n/locales/ar.json
#	src/i18n/locales/de.json
#	src/i18n/locales/es.json
#	src/i18n/locales/fr.json
#	src/i18n/locales/it.json
#	src/i18n/locales/ja.json
#	src/i18n/locales/pt.json
#	src/i18n/locales/ru.json
#	src/i18n/locales/uk.json
#	src/i18n/locales/zh-CN.json
#	src/i18n/locales/zh-Hant.json
#	src/ui/views/SceneChromeView.vue
2026-11-10 14:29:34 +04:00
German Khokhlov d9e34f10c6 Finish an interrupted frame event as soon as its error surfaces
Retry used to apply the queued listener changes of a Cesium event a
throwing listener left mid-raise, adds first and removes second. A layer
turned off and on while the notice was up landed in both queues and was
removed. The render and resize wrapper now finishes those events before
it raises the render error, so later adds and removes apply at once and
in order.
2026-11-10 14:28:01 +04:00
German Khokhlov 02a8442e6e Let Retry finish a frame event a throwing listener cut short
Cesium's Event.raiseEvent leaves an event mid-raise when a listener throws, so listeners added or removed afterwards stay queued. Retry now applies that queue for the frame and clock events: the notice's frame watchers see the next frame, and a listener removed since, such as a layer turned off, is not called again.
2026-11-06 14:45:42 +04:00
German Khokhlov 628de8386d Report thrown render loop errors and keep one loop after an inline Retry
An error thrown outside Scene's own error handling (a postRender listener,
the clock's onTick, a resize) stopped Cesium's loop without a renderError,
and with Cesium's panel turned off the globe froze silently. The notice now
raises such an error as a render error.

In an inline panel whose host stopped animation frames, Retry left the
panel's timer latched off, and once frames resumed a second render loop
drew every frame twice. The timer now follows Cesium's loop, and keeps the
loop marked running when its next frame is still queued.
2026-11-05 21:21:03 +04:00
German Khokhlov abfc85e03d Add the changelog entry for #1579 2026-11-05 13:03:42 +04:00
German Khokhlov fbec1488ac Show a recoverable notice when the globe stops drawing
Cesium stops its render loop after a render error and offers only a
panel that dismisses itself, and a lost WebGL context never draws again,
so the globe froze or went black while the controls kept responding.

The scene chrome now shows an alert dialog with the error. Retry restarts
the render loop and keeps the notice until a frame renders without an
error; after a lost context it reloads the page, whose address keeps the
view. Reset view opens the app root without the restored hash or share
path. An inline conversation panel never reloads its host page. The
application viewer turns off Cesium's own panel; other callers keep it.
2026-11-05 13:03:14 +04:00
58 changed files with 1859 additions and 1160 deletions

No files matched your search

@@ -0,0 +1,6 @@
- When the globe stops drawing after a render error or a lost WebGL context,
an alert dialog says so instead of leaving a frozen or black globe behind
working controls. Retry restarts drawing (Reload after a lost context), and
Reset view reopens the app without the view restored from the address bar.
Cesium's "Rendering has stopped" panel no longer appears in the app
(German, @gekh, #1579).
-7
View File
@@ -1,7 +0,0 @@
- Give counts real plural forms in every language: the wildlife count, the
Data Library list, the News time window, the GhostMaps event age and the
bridge radar range now read "1 Tag" and "7 Tage", «0,75 МОР. МИЛИ» and
«12 МОР. МИЛЬ». Street level words "the view centre" in each language's
grammar, and Latest, FOCUS, CONFIRMED and the course readout each have
their own translation where one word served two places (German, @gekh,
#1700).
+64
View File
@@ -2442,6 +2442,70 @@ future driver fix restores the effect with no code change; iOS/iPadOS platform
detection is the backstop for when no probe context can be created. Applied
during viewer construction, before any tile builds a draw command.
## Render failure notice
When the globe stops drawing, the scene chrome says so instead of leaving a
black globe behind controls that still respond. `installRenderFailureNotice()`
(`src/ui/renderFailure.js`) listens for Cesium's `renderError`, after which
Cesium stops its render loop, and for the canvas' `webglcontextlost`. It logs
the error and publishes a notice to the status store, which
`RenderFailure.vue` renders as an alert dialog that takes focus. When the
globe draws again, focus goes back to the element that had it, or, if that
element is gone or cannot take focus, to the globe's canvas, which the map's
keys (Enter to finish a drawing, Space to talk) read as the map. The canvas
gets `tabindex="-1"` only until focus moves on or the globe is pressed, and a
press leaves focus on the page, as Cesium has it. Focus the user moved
meanwhile stays put. The
application scene creates its viewer with `showRenderLoopErrors: false`, so
Cesium's own "Rendering has stopped" panel, which offers no way back, no longer
appears; `applicationViewerOptions()` and `createApplicationViewer()` keep
Cesium's panel by default for other callers. An error thrown outside Scene's
own error handling (a `preRender` or `postRender` listener, the clock's
`onTick`, a resize) also stops the loop without a `renderError`, so the notice
wraps the widget's `render()` and `resize()` and raises such an error as a
render error before Cesium's loop handles it as before; one Scene has raised
already and throws on (`rethrowRenderErrors`) is not raised again. A lost
context fails every later frame, and those errors are only logged, so the
notice keeps saying why. The notice sits above floating panels such as the
Québec camera viewer, and its buttons are 44 px tall on touch screens.
- **Retry** after a render error restarts the render loop. The notice stays
until a frame starts and ends without a render error (Cesium raises
`postRender` after a failed frame too), and a loop that fails again keeps it.
A listener that throws leaves its Cesium event mid-raise, with listeners
added or removed since only queued, so the `render()` and `resize()` wrapper
finishes the events it cut short (`finishRaise()`) before it raises the
render error. These are the events Cesium raises outside Scene's error
handling (`frameEvents()`): Scene's frame events, the camera's and the
clock's, the globe's tile load progress, and the load events of the scene's
top-level tilesets. So the notice's frame watchers start with the next
frame, a listener removed while the loop is stopped, for a layer turned off,
is not called again, and one turned off and on again keeps running.
- **Reload** after a lost context reloads the page: Cesium cannot draw into a
context it has lost, and the address keeps the view.
- **Reset view** opens the app's root address (`appRootUrl()`), without the
hash or `/share/<code>` path the view was restored from; the query, such as
`?lang=`, is kept.
An inline conversation panel never reloads or navigates its host's page, so it
offers Retry after a render error only, and nothing after a lost context, where
the panel runtime's own notice applies. The panel's timer
(`keepPanelRendering()`) draws only while Cesium's render loop is on, so it
stops after a render error and draws again after Retry. An error a frame of
the timer throws stops there: it has been raised as a render error, and
thrown on it would only reach the host page.
A render error marks Cesium's loop stopped, but a frame drawn while the loop
ran leaves its next animation frame queued: an error Scene catches itself
returns to the loop, which queues the frame, and a frame the panel's timer
draws leaves it queued. The `render()` and `resize()` wrapper keeps the loop
marked running after such a call, so a Retry before that frame, or a tab shown
again (`src/app/tools.js`), does not start a second loop that would draw every
frame twice. The queued frame stops the loop itself if nothing restarted it.
The `webglcontextlost` listener does not `preventDefault()` the context loss or
restore the context; it only reports it.
## Map Source control ownership
Map Source controls own chip listeners, source-state subscriptions and selection
+1 -2
View File
@@ -87,8 +87,7 @@ menu.onChoose = (code) => switchLocale(code); // switched in place and stored (i
- **Pass literals.** `t('Close')`, not `t(label)` (unless `label` holds a `msg()` value) and never a template literal with `${…}`.
- **Use placeholders, not concatenation.** `t('Flying to {place}…', { place })`, not `t('Flying to') + ' ' + place`. Word order differs between languages.
- **A placeholder takes a name, not a phrase.** When code has no name to pass and would fall back to a phrase ("the view centre", "this spot"), give that case a message of its own: `t('{source} at the view centre', { source })`. Other languages put a phrase in a different case after a preposition, which a name in its nominative form cannot follow.
- **Use `tn()` for counts**, not `count === 1 ? … : …`. Russian has three forms. That includes a number the English writes with a symbol or abbreviation, so both English forms read the same (`tn(hours, 'Last {hours} h', 'Last {hours} h', { hours })`): other languages inflect the words around it («Última 1 h», «Últimas 24 h»). Fractions have a form of their own too (ru «0,75 МОР. МИЛИ»).
- **Use `tn()` for counts**, not `count === 1 ? … : …`. Russian has three forms.
- **Translate the label, not the value.** Layer ids, enums, share-link parameters, API fields, voice tool schemas and feed data (callsigns, vessel and place names) stay as they are; translate only the text shown for them.
- **No `t()` at module top level.** Keep the English in `msg()` and call `t()` when rendering.
- **Numbers and dates** go through `formatNumber`, `formatDate`, `formatRelativeTime` and `formatUtcDayMonth` from `src/i18n/index.js`, which follow the active language.
+1 -1
View File
@@ -8,7 +8,7 @@ Catalog: `src/i18n/locales/ru.json`. General rules: [TRANSLATORS.md](../TRANSLAT
- Sentence case, not Title Case: "Live Flights" → «Рейсы в реальном времени».
- Quotes inside Russian text are «ёлочки». Write ё where it belongs: её, ещё, всё.
- In short labels, a standard abbreviation with a period beats a wrapped line: НЕОПРЕД., СЛЕД.
- Units: км, м, км/ч, м/с, уз (knots), фт (feet), ч, мин, с; in UPPERCASE text КМ, М, КМ/С, УЗ, ФТ. MW is МВт in any case. `°` and `%` stay. Nautical miles on the bridge radar are «мор. миля», a noun that follows the count: «3 МОР. МИЛИ», «12 МОР. МИЛЬ», «0,75 МОР. МИЛИ».
- Units: км, м, км/ч, м/с, уз (knots), фт (feet), ч, мин, с; in UPPERCASE text КМ, М, КМ/С, УЗ, ФТ. MW is МВт in any case. `°` and `%` stay.
## Plurals
+1 -1
View File
@@ -12,7 +12,7 @@ Translation source: meirm (#312).
- The apostrophe is always ʼ (U+02BC): обʼєкт, звʼязок, памʼять. Not ' or ’.
- Quotes inside Ukrainian text are «ялинки»; a button or layer name inside a sentence goes in them: Увімкніть «Вуличні знімки».
- In short labels, a standard abbreviation with a period beats a wrapped line: ПОПЕР., ДАЛЬН., КАЛІБР.
- Units: км, м, км/год, м/с, вуз (knots), фт (feet), год, хв, с, МГц, дБ, гПа; in UPPERCASE text КМ, М, КМ/С, ВУЗ, ФТ. Nautical miles on the bridge radar are «мор. миля», a noun that follows the count: «3 МОР. МИЛІ», «12 МОР. МИЛЬ», «0,75 МОР. МИЛІ». MW is МВт in any case. `°` and `%` stay. Decimal comma in coordinates written as text: 28,2710° пн. ш.
- Units: км, м, км/год, м/с, вуз (knots), фт (feet), год, хв, с, МГц, дБ, гПа; in UPPERCASE text КМ, М, КМ/С, ВУЗ, ФТ. MW is МВт in any case. `°` and `%` stay. Decimal comma in coordinates written as text: 28,2710° пн. ш.
- Coordinates: пн. ш. / пд. ш. / сх. д. / зх. д.; the HUD hemisphere letters are Пн, Пд, Сх, Зх.
## Plurals
+6
View File
@@ -557,6 +557,12 @@
"src/ui/frameRateMonitor.js",
"src/ui/frameRateMonitor.test.mjs",
"src/ui/frameRateView.test.mjs",
"src/ui/renderFailure.test.mjs",
"src/ui/renderFailureView.test.mjs",
"src/ui/renderFailureLoopErrors.test.mjs",
"src/ui/renderFailureEventRetry.test.mjs",
"src/app/renderFailureInlineRetry.test.mjs",
"src/ui/renderFailureSecondLoop.test.mjs",
"src/layers/cctv/lifecycle.js",
"src/layers/cctv/controls.js",
"src/layers/cctv/geometry.js",
+2
View File
@@ -163,6 +163,7 @@
"src/ui/kit/UiBadge.vue",
"src/ui/stores/globeActions.js",
"src/ui/views/panels/GlobeActions.vue",
"src/ui/views/panels/RenderFailure.vue",
"src/ui/views/panels/GlobeCompass.vue",
"src/ui/stores/recording.js",
"src/ui/styleParameters.js",
@@ -3113,6 +3114,7 @@
"src/ui/displayControls.js",
"src/ui/effects.js",
"src/ui/frameRateMonitor.js",
"src/ui/renderFailure.js",
"src/ui/layerPanel.js",
"src/ui/weatherPanel.js",
"src/ui/railCards.js",
@@ -400,6 +400,27 @@ test('Enter finishes from the canvas and the label field, never from a focused b
}
});
test('Enter on the globe canvas finishes, as after the render failure notice hands focus back', async () => {
const h = harness();
try {
h.act('toggleDraw');
h.tool.setShape('area');
h.tool.addVertex(-97.75, 30.26);
h.tool.addVertex(-97.74, 30.26);
h.tool.addVertex(-97.74, 30.27);
// src/ui/views/panels/RenderFailure.vue focuses the canvas when the globe
// draws again and the control that had focus is gone.
h.documentDouble.emit('keydown', { key: 'Enter', target: h.viewer.scene.canvas });
await Promise.resolve();
await Promise.resolve();
assert.equal(h.annotations.calls.annotate.length, 1, 'Enter on the canvas finishes');
assert.equal(h.annotations.calls.annotate[0].specs[0].type, 'area');
} finally {
h.restore();
}
});
test('the live preview hangs at the height the click landed on', async () => {
const h = harness();
try {
+13 -11
View File
@@ -122,28 +122,30 @@ export function keepPanelRendering(
frameRequest = windowRef.requestAnimationFrame(onFrame);
};
frameRequest = windowRef.requestAnimationFrame(onFrame);
// Cesium stops drawing after a render error, and so does the timer.
// Report the error in full: workers report plain objects, which Cesium
// prints as [object Object].
let failed = false;
// Cesium stops its render loop after a render error, and the timer stops
// with it; Retry restarts both. Report the error in full: workers report
// plain objects, which Cesium prints as [object Object].
const removeErrorListener = viewer.scene?.renderError?.addEventListener(
(_scene, error) => {
failed = true;
(_scene, error) =>
console.error(
"[God's Eye View panel] render error:",
describeError(error),
);
},
),
);
const timer = windowRef.setInterval(() => {
if (
failed ||
viewer.useDefaultRenderLoop === false ||
now() - lastFrame < MISSED_FRAMES_MS ||
viewer.isDestroyed?.()
)
return;
viewer.resize();
viewer.render();
try {
viewer.resize();
viewer.render();
} catch {
// Raised as a render error by now (src/ui/renderFailure.js), which
// stops this timer; thrown on, it would only reach the host page.
}
}, PANEL_FRAME_MS);
return () => {
windowRef.clearInterval(timer);
+2
View File
@@ -482,6 +482,8 @@ test('a panel reports a render error in full and stops drawing', () => {
try {
let clock = 0;
const stop = keepPanelRendering(viewer, { windowRef, now: () => clock });
// Cesium stops its render loop after a render error.
viewer.useDefaultRenderLoop = false;
listener(viewer.scene, { name: 'RuntimeError', message: 'worker failed' });
assert.match(logged[0], /render error: .*"message":"worker failed"/);
clock = 1000;
+156
View File
@@ -0,0 +1,156 @@
// Retry in an inline panel whose host stops animation frames, so the panel
// draws from keepPanelRendering's timer. Drives Cesium's real startRenderLoop
// and useDefaultRenderLoop setter (CesiumWidget.prototype).
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { keepPanelRendering } from './embed.js';
import { installRenderFailureNotice } from '../ui/renderFailure.js';
function setup() {
const frames = [];
let tick = null;
let clock = 0;
// The host reports the panel hidden: its own animation frames never arrive.
const windowRef = {
requestAnimationFrame: () => 1,
cancelAnimationFrame() {},
setInterval: (fn) => ((tick = fn), 1),
clearInterval() {},
};
const previousRaf = globalThis.requestAnimationFrame;
globalThis.requestAnimationFrame = (fn) => frames.push(fn);
const previousError = console.error;
console.error = () => {};
const state = { failure: null, drawn: 0 };
const scene = {
renderError: new Cesium.Event(),
preUpdate: new Cesium.Event(),
postRender: new Cesium.Event(),
requestRender() {},
initializeFrame() {},
// Scene.render: a failing primitive update raises renderError
// (tryAndCatchError) and returns; a failing listener throws.
render() {
state.drawn += 1;
this.preUpdate.raiseEvent(this);
if (state.failure === 'caught')
this.renderError.raiseEvent(this, new Error('boom'));
if (state.failure === 'thrown') throw new Error('boom');
this.postRender.raiseEvent(this);
},
};
const widget = Object.create(Cesium.CesiumWidget.prototype);
Object.assign(widget, {
_scene: scene,
_clock: { tick() {} },
_canRender: true,
_useDefaultRenderLoop: false,
_renderLoopRunning: false,
_showRenderLoopErrors: false,
_targetFrameRate: undefined,
isDestroyed: () => false,
resize() {},
});
// CesiumWidget's own renderError handler, which marks the loop stopped.
scene.renderError.addEventListener(() => {
widget._useDefaultRenderLoop = false;
widget._renderLoopRunning = false;
});
const viewer = {
scene,
cesiumWidget: widget,
canvas: { addEventListener() {}, removeEventListener() {} },
get useDefaultRenderLoop() {
return widget.useDefaultRenderLoop;
},
set useDefaultRenderLoop(value) {
widget.useDefaultRenderLoop = value;
},
isDestroyed: () => false,
render: () => widget.render(),
resize: () => widget.resize(),
};
const store = {
renderFailure: null,
publishRenderFailure(next) {
this.renderFailure = next;
},
};
const uninstall = installRenderFailureNotice(viewer, {
store,
allowReload: false,
locationRef: { href: 'https://gev.example/', reload() {}, assign() {} },
logError() {},
});
const stop = keepPanelRendering(viewer, { windowRef, now: () => clock });
viewer.useDefaultRenderLoop = true;
const timerFrame = () => {
clock += 400;
tick();
};
const animationFrames = (n) => {
for (let frame = 1; frame <= n; frame += 1)
for (const render of frames.splice(0)) render(frame * 16);
};
const restore = () => {
stop();
uninstall();
globalThis.requestAnimationFrame = previousRaf;
console.error = previousError;
};
return { frames, state, store, timerFrame, animationFrames, restore };
}
for (const failure of ['caught', 'thrown']) {
test(`an error ${failure} in a frame the timer draws: Retry draws again with one render loop`, () => {
const { frames, state, store, timerFrame, animationFrames, restore } =
setup();
try {
// The host stops animation frames with the loop's next one queued.
assert.equal(frames.length, 1);
state.failure = failure;
timerFrame(); // an error thrown here does not reach the host page
assert.equal(store.renderFailure?.reason, 'render-error');
assert.equal(typeof store.renderFailure.retry, 'function');
state.failure = null;
store.renderFailure.retry();
const before = state.drawn;
timerFrame();
assert.ok(state.drawn > before, 'the panel draws again after Retry');
assert.equal(
store.renderFailure,
null,
'a clean frame clears the notice',
);
assert.equal(frames.length, 1, 'Retry starts no second loop');
// The host resumes frames: the globe draws once a frame.
state.drawn = 0;
animationFrames(3);
assert.equal(state.drawn, 3);
} finally {
restore();
}
});
}
test('an error Scene caught in an animation frame right before the host stopped frames: Retry keeps one render loop', () => {
const { frames, state, store, timerFrame, animationFrames, restore } =
setup();
try {
state.failure = 'caught';
animationFrames(1);
assert.equal(store.renderFailure?.reason, 'render-error');
assert.equal(frames.length, 1);
state.failure = null;
store.renderFailure.retry();
timerFrame();
assert.equal(store.renderFailure, null);
assert.equal(frames.length, 1, 'Retry starts no second loop');
state.drawn = 0;
animationFrames(3);
assert.equal(state.drawn, 3);
} finally {
restore();
}
});
+8
View File
@@ -15,6 +15,8 @@ import {
governorRequestRender,
} from '../renderGovernor.js';
import { describeError } from './errors.js';
import { isEmbeddedInline } from './embed.js';
import { installRenderFailureNotice } from '../ui/renderFailure.js';
import { worldClock, worldTimeFor } from '../time/worldClock.js';
import { followWorldTime } from '../time/viewerClock.js';
import { onLocaleChange, t } from '../i18n/index.js';
@@ -55,11 +57,17 @@ export async function createApplicationScene({
const viewer = createApplicationViewer({
container: 'cesiumContainer',
creditContainer,
// The render failure notice below replaces Cesium's dead-end panel.
showRenderLoopErrors: false,
});
defer(() => {
uninstallRenderGovernor(viewer);
if (!viewer.isDestroyed()) viewer.destroy();
});
// An inline panel must not reload or navigate its host's page.
defer(
installRenderFailureNotice(viewer, { allowReload: !isEmbeddedInline() }),
);
defer(installTrackpadPinchZoom(viewer));
defer(
followWorldTime(viewer, worldTimeFor('sky'), {
+16 -4
View File
@@ -375,8 +375,15 @@ export function installGlobeInViewRecovery(
};
}
/** Cesium.Viewer options of the standard globe viewer. */
export function applicationViewerOptions(creditContainer) {
/**
* Cesium.Viewer options of the standard globe viewer. `showRenderLoopErrors`
* false leaves render errors to the caller's own notice instead of Cesium's
* panel.
*/
export function applicationViewerOptions(
creditContainer,
{ showRenderLoopErrors = true } = {},
) {
return {
timeline: false,
animation: false,
@@ -396,16 +403,21 @@ export function applicationViewerOptions(creditContainer) {
creditContainer,
msaaSamples: 4,
contextOptions: { webgl: { preserveDrawingBuffer: true } },
showRenderLoopErrors,
};
}
/** Create the standard globe viewer in caller-owned, visible containers. */
export function createApplicationViewer({ container, creditContainer }) {
export function createApplicationViewer({
container,
creditContainer,
showRenderLoopErrors,
}) {
if (!container || !creditContainer)
throw new TypeError('Viewer and credit containers are required');
const viewer = new Cesium.Viewer(
container,
applicationViewerOptions(creditContainer),
applicationViewerOptions(creditContainer, { showRenderLoopErrors }),
);
try {
viewer.targetFrameRate = 60;
+19 -90
View File
@@ -77,22 +77,8 @@
"{severity} only": "{severity} فقط",
"All categories": "كل الفئات",
"Reading the news…": "جارٍ قراءة الأخبار…",
"News, last {hours} hours": {
"zero": "الأخبار، آخر {hours} ساعة",
"one": "الأخبار، آخر ساعة ({hours})",
"two": "الأخبار، آخر ساعتين ({hours})",
"few": "الأخبار، آخر {hours} ساعات",
"many": "الأخبار، آخر {hours} ساعة",
"other": "الأخبار، آخر {hours} ساعة"
},
"Last {hours} h": {
"zero": "آخر {hours} ساعة",
"one": "آخر ساعة ({hours})",
"two": "آخر ساعتين ({hours})",
"few": "آخر {hours} ساعات",
"many": "آخر {hours} ساعة",
"other": "آخر {hours} ساعة"
},
"News, last {hours} hours": "الأخبار، الساعات الأخيرة: {hours}",
"Last {hours} h": "آخر {hours} س",
"Time window": "الفترة الزمنية",
"Conflicts": "النزاعات",
"Local": "محلي",
@@ -101,23 +87,9 @@
"Hotspots": "بؤر ساخنة",
"Severity": "الخطورة",
"Category": "الفئة",
"Nothing reported near here in the last {hours} h": {
"zero": "لم يُبلَّغ عن شيء قريب من هنا في آخر {hours} ساعة",
"one": "لم يُبلَّغ عن شيء قريب من هنا في آخر ساعة ({hours})",
"two": "لم يُبلَّغ عن شيء قريب من هنا في آخر ساعتين ({hours})",
"few": "لم يُبلَّغ عن شيء قريب من هنا في آخر {hours} ساعات",
"many": "لم يُبلَّغ عن شيء قريب من هنا في آخر {hours} ساعة",
"other": "لم يُبلَّغ عن شيء قريب من هنا في آخر {hours} ساعة"
},
"Nothing reported near here in the last {hours} h": "لم يُبلَّغ عن شيء قريب من هنا في آخر {hours} س",
"Latest news": "آخر الأخبار",
"No news matches these filters in the last {hours} h": {
"zero": "لا أخبار تطابق هذه المرشحات في آخر {hours} ساعة",
"one": "لا أخبار تطابق هذه المرشحات في آخر ساعة ({hours})",
"two": "لا أخبار تطابق هذه المرشحات في آخر ساعتين ({hours})",
"few": "لا أخبار تطابق هذه المرشحات في آخر {hours} ساعات",
"many": "لا أخبار تطابق هذه المرشحات في آخر {hours} ساعة",
"other": "لا أخبار تطابق هذه المرشحات في آخر {hours} ساعة"
},
"No news matches these filters in the last {hours} h": "لا أخبار تطابق هذه المرشحات في آخر {hours} س",
"Load older news": "تحميل أخبار أقدم",
"How to read the map": "كيف تقرأ الخريطة",
"Colour: severity · icon: type of event": "اللون: الخطورة · الرمز: نوع الحدث",
@@ -287,14 +259,7 @@
"ALTITUDE": "الارتفاع",
"CONTACT": "الهدف",
"CONTACTS · 250 KM": "الأهداف · 250 كم",
"RINGS {spacing} NM": {
"zero": "الحلقات كل {spacing} ميل بحري",
"one": "الحلقات كل ميل بحري ({spacing})",
"two": "الحلقات كل ميلين بحريين ({spacing})",
"few": "الحلقات كل {spacing} أميال بحرية",
"many": "الحلقات كل {spacing} ميلًا بحريًا",
"other": "الحلقات كل {spacing} ميل بحري"
},
"RINGS {spacing} NM": "الحلقات كل {spacing} ميل بحري",
"{count} CONTACTS": {
"zero": "لا أهداف ({count})",
"one": "هدف واحد ({count})",
@@ -423,14 +388,7 @@
"ROUTE DATA UNAVAILABLE": "بيانات المسار غير متاحة",
"CONTACT NAVIGATION OFF": "التنقل بين الأهداف متوقف",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "تبقى غرفة القيادة مع السفينة المحددة",
"{range} NM": {
"zero": "{range} ميل بحري",
"one": "ميل بحري واحد ({range})",
"two": "ميلان بحريان ({range})",
"few": "{range} أميال بحرية",
"many": "{range} ميلًا بحريًا",
"other": "{range} ميل بحري"
},
"{range} NM": "{range} ميل بحري",
"AIS DESTINATION · CREW REPORTED": "وجهة AIS · أبلغ عنها الطاقم",
"FROM": "من",
"TO": "إلى",
@@ -514,7 +472,6 @@
"Current style: {style} — click for next": "النمط الحالي: {style} — انقر للتالي",
"TRACK ACQUIRED": "تم التقاط الهدف",
"{label} · COURSE {heading}°": "{label} · المسار {heading}°",
"vessel|{label} · COURSE {heading}°": "{label} · المسار {heading}°",
"{label} · HEADING {heading}°": "{label} · الاتجاه {heading}°",
"{label} · NO HEADING": "{label} · لا اتجاه",
"{label} · NADIR VIEW": "{label} · عرض باتجاه النظير",
@@ -716,7 +673,6 @@
"NEAREST": "الأقرب",
"CCTV camera": "كاميرا CCTV",
"FOCUS": "تركيز",
"site status|FOCUS": "تركيز",
"COVERAGE OFF": "التغطية متوقفة",
"AUTO HOP OFF": "التنقل التلقائي متوقف",
"PROJECTION ON": "الإسقاط مفعّل",
@@ -1107,7 +1063,6 @@
"awaiting first AIS message…": "بانتظار أول رسالة AIS…",
"EST. DOWNRANGE": "تقدير على امتداد المسار",
"CONFIRMED": "مؤكَّد",
"sightings filter|CONFIRMED": "مؤكَّدة",
"PAD / RTLS": "منصة الإطلاق / RTLS",
"Pick a shape, then click the map.": "اختر شكلًا، ثم انقر على الخريطة.",
"Enter to place the pin, Esc to cancel.": "Enter لوضع الدبوس، وEsc للإلغاء.",
@@ -2589,7 +2544,6 @@
"Observed history": "سجل الأرصاد",
"Later observation": "رصد لاحق",
"Latest": "الأحدث",
"news list|Latest": "الأحدث",
"Newest frame per product": "أحدث إطار لكل منتج",
"Replay observed history": "إعادة تشغيل سجل الأرصاد",
"Imagery on Esri · Google 3D returns when cleared": "الصور على Esri · تعود Google 3D عند المسح",
@@ -3762,14 +3716,7 @@
"Photo of {name}": "صورة {name}",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": {
"zero": "{count} مشاهدة في نطاق العرض",
"one": "مشاهدة واحدة ({count}) في نطاق العرض",
"two": "مشاهدتان ({count}) في نطاق العرض",
"few": "{count} مشاهدات في نطاق العرض",
"many": "{count} مشاهدة في نطاق العرض",
"other": "{count} مشاهدة في نطاق العرض"
},
"{count} sightings in view": "المشاهدات في نطاق العرض: {count}",
"Close sighting": "إغلاق المشاهدة",
"Wildlife observed within 48 h (whales 7 d). Color and badge glyph show the taxon group. Sensitive species are shown only as 0.2° cells; Salish Sea marine mammal reports via Acartia are rounded to 0.01°. Not a census.": "حياة برية شوهدت خلال 48 ساعة (الحيتان 7 أيام). يوضح اللون ورمز الشارة المجموعة التصنيفية. تُعرض الأنواع الحساسة في خلايا بحجم 0.2° فقط؛ وتُقرّب مشاهدات الثدييات البحرية في بحر ساليش عبر Acartia إلى 0.01°. ليست إحصاءً شاملًا.",
"Open {name} on iNaturalist": "فتح {name} على iNaturalist",
@@ -3780,37 +3727,20 @@
"{days}d": "{days} ي",
"All markers": "كل العلامات",
"Show markers of every age": "عرض العلامات من جميع الأعمار",
"Maximum event age: {days}d; undated markers are kept": {
"zero": "الحد الأقصى لعمر الحدث: {days} يوم؛ تبقى العلامات غير المؤرخة ظاهرة",
"one": "الحد الأقصى لعمر الحدث: يوم واحد ({days})؛ تبقى العلامات غير المؤرخة ظاهرة",
"two": "الحد الأقصى لعمر الحدث: يومان ({days})؛ تبقى العلامات غير المؤرخة ظاهرة",
"few": "الحد الأقصى لعمر الحدث: {days} أيام؛ تبقى العلامات غير المؤرخة ظاهرة",
"many": "الحد الأقصى لعمر الحدث: {days} يومًا؛ تبقى العلامات غير المؤرخة ظاهرة",
"other": "الحد الأقصى لعمر الحدث: {days} يوم؛ تبقى العلامات غير المؤرخة ظاهرة"
},
"Maximum event age: {days}d; undated markers are kept": "الحد الأقصى لعمر الحدث: {days} ي؛ تبقى العلامات غير المؤرخة ظاهرة",
"event age filter|All": "الكل",
"Event age": "عمر الحدث",
"Event age: {days}d · undated kept": {
"zero": "عمر الحدث: {days} يوم · غير المؤرخة تبقى ظاهرة",
"one": "عمر الحدث: يوم واحد ({days}) · غير المؤرخة تبقى ظاهرة",
"two": "عمر الحدث: يومان ({days}) · غير المؤرخة تبقى ظاهرة",
"few": "عمر الحدث: {days} أيام · غير المؤرخة تبقى ظاهرة",
"many": "عمر الحدث: {days} يومًا · غير المؤرخة تبقى ظاهرة",
"other": "عمر الحدث: {days} يوم · غير المؤرخة تبقى ظاهرة"
},
"Event age: {days}d · undated kept": "عمر الحدث: {days} ي · غير المؤرخة تبقى ظاهرة",
"Close camera preview": "إغلاق معاينة الكاميرا",
"Refresh map still": "تحديث صورة الخريطة الثابتة",
"The map still uses a separate request and may show a different moment.": "تستخدم صورة الخريطة طلبًا منفصلًا وقد تعرض لحظة مختلفة.",
"Video plays from Québec 511, which sees your IP address.": "يُشغَّل الفيديو من Québec 511، الذي يرى عنوان IP الخاص بك.",
"Press OPEN to load the photo": "اضغط «فتح» لتحميل الصورة",
"Captured {month}": "التُقطت في {month}",
"this spot": "هذا الموضع",
"the view centre": "مركز العرض",
"{source} at {place}": "{source} في {place}",
"{source} at this spot": "{source} في هذا الموضع",
"{source} at the view centre": "{source} في مركز العرض",
"Street level at {place}": "مستوى الشارع في {place}",
"Street level at this spot": "مستوى الشارع في هذا الموضع",
"Street level at the view centre": "مستوى الشارع في مركز العرض",
"Switching map": "جارٍ تبديل الخريطة",
"Opening imagery": "جارٍ فتح الصور",
"Street View is ready: press {open} to load it": "Street View جاهز: اضغط «{open}» لتحميله",
@@ -3902,7 +3832,6 @@
"{source} imagery could not be opened": "تعذّر فتح صور {source}",
"{place} has no position to look for imagery at": "{place}: لا موضع للبحث عن الصور فيه",
"No {source} imagery near {place}": "لا توجد صور {source} قرب {place}",
"No {source} imagery near the view centre": "لا توجد صور {source} قرب مركز العرض",
"{reason} (filters on: {filters})": "{reason} (المرشحات المفعّلة: {filters})",
"tiles unavailable": "البلاطات غير متاحة",
"zoom in to load this layer": "قرّب لتحميل هذه الطبقة",
@@ -4023,13 +3952,13 @@
"REMOVE ALL": "إزالة الكل",
"Confirm: remove every added entry from Data Layers": "تأكيد: إزالة كل الإدخالات المُضافة من طبقات البيانات",
"Remove every added entry from Data Layers": "إزالة كل الإدخالات المُضافة من طبقات البيانات",
"Showing {shown} of {total}": {
"zero": "عرض {shown} من {total}",
"one": "عرض {shown} من {total}",
"two": "عرض {shown} من {total}",
"few": "عرض {shown} من {total}",
"many": "عرض {shown} من {total}",
"other": "عرض {shown} من {total}"
},
"BROWSE DATA LIBRARY": "تصفّح مكتبة البيانات"
"Showing {shown} of {total}": "عرض {shown} من {total}",
"BROWSE DATA LIBRARY": "تصفّح مكتبة البيانات",
"The globe stopped drawing": "توقفت الكرة الأرضية عن الرسم",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "استعاد المتصفح رسومات الكرة الأرضية ثلاثية الأبعاد. لا تزال الطبقات وعناصر التحكم تعمل، لكن الخريطة لن تُرسم حتى تُعاد تحميل الصفحة.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "توقف العرض بعد خطأ. لا تزال الطبقات وعناصر التحكم تعمل، لكن الخريطة لا يتم تحديثها.",
"Reset view reloads the app without the view restored from the address bar.": "تعيد «إعادة ضبط العرض» تحميل التطبيق دون العرض المستعاد من شريط العنوان.",
"Reload": "إعادة التحميل",
"Retry": "إعادة المحاولة",
"Reset view": "إعادة ضبط العرض"
}
+20 -51
View File
@@ -65,14 +65,8 @@
"{severity} only": "Nur {severity}",
"All categories": "Alle Kategorien",
"Reading the news…": "Nachrichten werden gelesen…",
"News, last {hours} hours": {
"one": "Nachrichten der letzten {hours} Stunde",
"other": "Nachrichten der letzten {hours} Stunden"
},
"Last {hours} h": {
"one": "Letzte {hours} h",
"other": "Letzte {hours} h"
},
"News, last {hours} hours": "Nachrichten, letzte {hours} h",
"Last {hours} h": "Letzte {hours} h",
"Time window": "Zeitraum",
"Conflicts": "Konflikte",
"Local": "Lokal",
@@ -81,15 +75,9 @@
"Hotspots": "Brennpunkte",
"Severity": "Schweregrad",
"Category": "Kategorie",
"Nothing reported near here in the last {hours} h": {
"one": "In der letzten {hours} h nichts in der Nähe gemeldet",
"other": "In den letzten {hours} h nichts in der Nähe gemeldet"
},
"Nothing reported near here in the last {hours} h": "In den letzten {hours} h nichts in der Nähe gemeldet",
"Latest news": "Neueste Nachrichten",
"No news matches these filters in the last {hours} h": {
"one": "Keine Nachrichten der letzten {hours} h passen zu diesen Filtern",
"other": "Keine Nachrichten der letzten {hours} h passen zu diesen Filtern"
},
"No news matches these filters in the last {hours} h": "Keine Nachrichten der letzten {hours} h passen zu diesen Filtern",
"Load older news": "Ältere Nachrichten laden",
"How to read the map": "So liest man die Karte",
"Colour: severity · icon: type of event": "Farbe: Schweregrad · Symbol: Art des Ereignisses",
@@ -210,7 +198,6 @@
"Previous — prior visited contact in the 250 km window": "Zurück: zuvor besuchter Kontakt im 250-km-Fenster",
"PREVIOUS": "ZURÜCK",
"FOCUS": "FOKUS",
"site status|FOCUS": "FOKUS",
"Next — nearest unvisited contact in the 250 km window": "Weiter: nächstgelegener unbesuchter Kontakt im 250-km-Fenster",
"NEXT": "WEITER",
"Open-source mapped/observed context. Missing broadcasts, unloaded map areas, or unmapped sites are not evidence of absence.": "Kontext aus offenen Quellen, kartiert/beobachtet. Fehlende Funksignale, nicht geladene Kartenbereiche oder nicht kartierte Standorte sind kein Beleg für Abwesenheit.",
@@ -315,7 +302,7 @@
"ORBITAL VELOCITY": "BAHNGESCHWINDIGKEIT",
"SUB-SATELLITE POINT": "SUBSATELLITENPUNKT",
"NORTH UP": "NORDEN OBEN",
"CURRENTLY OVER": "UNTER DEM SATELLITEN",
"CURRENTLY OVER": "AKTUELL ÜBER",
"{division}, {country}": "{division}, {country}",
"{region} · near {place}": "{region} · nahe {place}",
"NORAD {id}": "NORAD {id}",
@@ -572,10 +559,7 @@
"ROUTE DATA UNAVAILABLE": "ROUTENDATEN NICHT VERFÜGBAR",
"CONTACT NAVIGATION OFF": "KONTAKTNAVIGATION AUS",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "DIE BRÜCKE BLEIBT BEIM GEWÄHLTEN SCHIFF",
"{range} NM": {
"one": "{range} SM",
"other": "{range} SM"
},
"{range} NM": "{range} SM",
"AIS DESTINATION · CREW REPORTED": "AIS-ZIEL · VON DER BESATZUNG GEMELDET",
"DEST {bearing}": "DEST {bearing}",
"Disable cockpit weather effects": "Cockpit-Wettereffekte deaktivieren",
@@ -621,7 +605,6 @@
"Reclassify as TR-3B": "Als TR-3B umklassifizieren",
"TRACK ACQUIRED": "TRACK ERFASST",
"{label} · COURSE {heading}°": "{label} · KURS {heading}°",
"vessel|{label} · COURSE {heading}°": "{label} · KURS {heading}°",
"{label} · HEADING {heading}°": "{label} · STEUERKURS {heading}°",
"{label} · NO HEADING": "{label} · KEIN STEUERKURS",
"{label} · NADIR VIEW": "{label} · NADIRANSICHT",
@@ -853,10 +836,7 @@
"Contact cockpit summary": "Cockpit-Zusammenfassung des Kontakts",
"CONTACT": "KONTAKT",
"CONTACTS · 250 KM": "KONTAKTE · 250 KM",
"RINGS {spacing} NM": {
"one": "RINGE {spacing} SM",
"other": "RINGE {spacing} SM"
},
"RINGS {spacing} NM": "RINGE {spacing} SM",
"{count} CONTACTS": {
"one": "{count} KONTAKT",
"other": "{count} KONTAKTE"
@@ -2424,7 +2404,6 @@
"Observed history": "Beobachteter Verlauf",
"Later observation": "Spätere Beobachtung",
"Latest": "Neueste",
"news list|Latest": "Neueste",
"Newest frame per product": "Neuester Frame je Produkt",
"Replay observed history": "Beobachteten Verlauf abspielen",
"Imagery on Esri · Google 3D returns when cleared": "Bilder auf Esri · nach LÖSCHEN wieder Google 3D",
@@ -3153,7 +3132,6 @@
"Left panels": "Linke Bereiche",
"Right panels": "Rechte Bereiche",
"CONFIRMED": "BESTÄTIGT",
"sightings filter|CONFIRMED": "BESTÄTIGT",
"PAD / RTLS": "STARTPLATZ / RTLS",
"fire confidence filter|All": "Alle",
"fire confidence filter|Nominal+": "Mittel+",
@@ -3414,10 +3392,7 @@
"Photo of {name}": "Foto von {name}",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": {
"one": "{count} Beobachtung im Bild",
"other": "{count} Beobachtungen im Bild"
},
"{count} sightings in view": "Beobachtungen im Bild: {count}",
"Close sighting": "Beobachtung schließen",
"Wildlife observed within 48 h (whales 7 d). Color and badge glyph show the taxon group. Sensitive species are shown only as 0.2° cells; Salish Sea marine mammal reports via Acartia are rounded to 0.01°. Not a census.": "In den letzten 48 h beobachtete Wildtiere (Wale 7 Tage). Farbe und Symbol der Markierung zeigen die Artengruppe. Empfindliche Arten erscheinen nur als 0,2°-Zellen; Acartia-Meldungen zu Meeressäugern aus der Salish-See werden auf 0,01° gerundet. Keine Bestandszählung.",
"Open {name} on iNaturalist": "{name} auf iNaturalist öffnen",
@@ -3428,29 +3403,20 @@
"{days}d": "{days} T",
"All markers": "Alle Markierungen",
"Show markers of every age": "Markierungen jeden Alters anzeigen",
"Maximum event age: {days}d; undated markers are kept": {
"one": "Maximales Ereignisalter: {days} Tag; undatierte Markierungen bleiben erhalten",
"other": "Maximales Ereignisalter: {days} Tage; undatierte Markierungen bleiben erhalten"
},
"Maximum event age: {days}d; undated markers are kept": "Maximales Ereignisalter: {days} T; undatierte Markierungen bleiben erhalten",
"event age filter|All": "Alle",
"Event age": "Ereignisalter",
"Event age: {days}d · undated kept": {
"one": "Ereignisalter: {days} Tag · undatierte bleiben",
"other": "Ereignisalter: {days} Tage · undatierte bleiben"
},
"Event age: {days}d · undated kept": "Ereignisalter: {days} T · undatierte bleiben",
"Close camera preview": "Kameravorschau schließen",
"Refresh map still": "Kartenstandbild aktualisieren",
"The map still uses a separate request and may show a different moment.": "Das Kartenstandbild wird separat abgerufen und kann einen anderen Moment zeigen.",
"Video plays from Québec 511, which sees your IP address.": "Das Video wird von Québec 511 abgespielt; die IP-Adresse ist für diesen Anbieter sichtbar.",
"Press OPEN to load the photo": "Auf ÖFFNEN klicken, um das Foto zu laden",
"Captured {month}": "Aufgenommen im {month}",
"this spot": "diese Stelle",
"the view centre": "die Bildmitte",
"{source} at {place}": "{source}: {place}",
"{source} at this spot": "{source} an dieser Stelle",
"{source} at the view centre": "{source} in der Bildmitte",
"Street level at {place}": "Straßenebene: {place}",
"Street level at this spot": "Straßenebene an dieser Stelle",
"Street level at the view centre": "Straßenebene in der Bildmitte",
"Switching map": "Karte wird gewechselt",
"Opening imagery": "Bilder werden geöffnet",
"Street View is ready: press {open} to load it": "Street View ist bereit: Auf {open} klicken, um es zu laden",
@@ -3542,7 +3508,6 @@
"{source} imagery could not be opened": "Bilder von {source} konnten nicht geöffnet werden",
"{place} has no position to look for imagery at": "{place} hat keine Position, an der nach Bildern gesucht werden kann",
"No {source} imagery near {place}": "Keine Bilder von {source} in der Nähe von {place}",
"No {source} imagery near the view centre": "Keine Bilder von {source} in der Nähe der Bildmitte",
"{reason} (filters on: {filters})": "{reason} (aktive Filter: {filters})",
"tiles unavailable": "Kacheln nicht verfügbar",
"zoom in to load this layer": "hineinzoomen, um diese Ebene zu laden",
@@ -3639,9 +3604,13 @@
"REMOVE ALL": "ALLE ENTFERNEN",
"Confirm: remove every added entry from Data Layers": "Bestätigen: alle hinzugefügten Einträge aus Datenebenen entfernen",
"Remove every added entry from Data Layers": "Alle hinzugefügten Einträge aus Datenebenen entfernen",
"Showing {shown} of {total}": {
"one": "{shown} von {total} angezeigt",
"other": "{shown} von {total} angezeigt"
},
"BROWSE DATA LIBRARY": "DATENBIBLIOTHEK DURCHSUCHEN"
"Showing {shown} of {total}": "{shown} von {total} angezeigt",
"BROWSE DATA LIBRARY": "DATENBIBLIOTHEK DURCHSUCHEN",
"The globe stopped drawing": "Der Globus zeichnet nicht mehr",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "Der Browser hat dem Globus die 3D-Grafik entzogen. Ebenen und Bedienelemente funktionieren weiter, aber die Karte kann erst nach dem Neuladen der Seite wieder zeichnen.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "Das Rendern wurde nach einem Fehler angehalten. Ebenen und Bedienelemente funktionieren weiter, aber die Karte wird nicht aktualisiert.",
"Reset view reloads the app without the view restored from the address bar.": "Ansicht zurücksetzen lädt die App ohne die aus der Adressleiste wiederhergestellte Ansicht neu.",
"Reload": "Neu laden",
"Retry": "Erneut versuchen",
"Reset view": "Ansicht zurücksetzen"
}
+19 -60
View File
@@ -68,16 +68,8 @@
"{severity} only": "Solo {severity}",
"All categories": "Todas las categorías",
"Reading the news…": "Leyendo las noticias…",
"News, last {hours} hours": {
"one": "Noticias de la última {hours} hora",
"many": "Noticias de las últimas {hours} de horas",
"other": "Noticias de las últimas {hours} horas"
},
"Last {hours} h": {
"one": "Última {hours} h",
"many": "Últimas {hours} h",
"other": "Últimas {hours} h"
},
"News, last {hours} hours": "Noticias desde hace {hours} h",
"Last {hours} h": "Desde hace {hours} h",
"Time window": "Periodo",
"Conflicts": "Conflictos",
"Local": "Local",
@@ -86,17 +78,9 @@
"Hotspots": "Focos",
"Severity": "Gravedad",
"Category": "Categoría",
"Nothing reported near here in the last {hours} h": {
"one": "No hay noticias cerca de aquí en la última {hours} h",
"many": "No hay noticias cerca de aquí en las últimas {hours} h",
"other": "No hay noticias cerca de aquí en las últimas {hours} h"
},
"Nothing reported near here in the last {hours} h": "No hay noticias cerca de aquí desde hace {hours} h",
"Latest news": "Últimas noticias",
"No news matches these filters in the last {hours} h": {
"one": "En la última {hours} h no hay noticias que coincidan con estos filtros",
"many": "En las últimas {hours} h no hay noticias que coincidan con estos filtros",
"other": "En las últimas {hours} h no hay noticias que coincidan con estos filtros"
},
"No news matches these filters in the last {hours} h": "Desde hace {hours} h no hay noticias que coincidan con estos filtros",
"Load older news": "Cargar noticias anteriores",
"How to read the map": "Cómo leer el mapa",
"Colour: severity · icon: type of event": "Color: gravedad · icono: tipo de suceso",
@@ -155,11 +139,7 @@
"ALTITUDE": "ALTITUD",
"CONTACT": "CONTACTO",
"CONTACTS · 250 KM": "CONTACTOS · 250 KM",
"RINGS {spacing} NM": {
"one": "ANILLOS {spacing} MN",
"many": "ANILLOS {spacing} MN",
"other": "ANILLOS {spacing} MN"
},
"RINGS {spacing} NM": "ANILLOS {spacing} MN",
"{count} CONTACTS": {
"one": "{count} CONTACTO",
"many": "{count} DE CONTACTOS",
@@ -286,11 +266,7 @@
"ROUTE DATA UNAVAILABLE": "DATOS DE RUTA NO DISPONIBLES",
"CONTACT NAVIGATION OFF": "NAVEGACIÓN DE CONTACTOS DESACTIVADA",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "EL PUENTE PERMANECE EN EL BUQUE SELECCIONADO",
"{range} NM": {
"one": "{range} MN",
"many": "{range} MN",
"other": "{range} MN"
},
"{range} NM": "{range} MN",
"AIS DESTINATION · CREW REPORTED": "DESTINO AIS · NOTIFICADO POR LA TRIPULACIÓN",
"FROM": "DESDE",
"TO": "HACIA",
@@ -376,7 +352,6 @@
"Current style: {style} — click for next": "Estilo actual: {style} — haz clic para el siguiente",
"TRACK ACQUIRED": "RASTREO ADQUIRIDO",
"{label} · COURSE {heading}°": "{label} · RUMBO {heading}°",
"vessel|{label} · COURSE {heading}°": "{label} · RUMBO {heading}°",
"{label} · HEADING {heading}°": "{label} · PROA {heading}°",
"{label} · NO HEADING": "{label} · PROA N/D",
"{label} · NADIR VIEW": "{label} · VISTA AL NADIR",
@@ -571,7 +546,6 @@
"NEAREST": "MÁS CERCANA",
"CCTV camera": "Cámara CCTV",
"FOCUS": "ENFOCAR",
"site status|FOCUS": "ENFOCADO",
"COVERAGE OFF": "COBERTURA DESACTIVADA",
"AUTO HOP OFF": "SALTO AUTO: NO",
"PROJECTION ON": "PROYECCIÓN ACTIVADA",
@@ -925,7 +899,6 @@
"awaiting first AIS message…": "esperando el primer mensaje AIS…",
"EST. DOWNRANGE": "DIST. ESTIMADA",
"CONFIRMED": "CONFIRMADA",
"sightings filter|CONFIRMED": "CONFIRMADAS",
"PAD / RTLS": "PLATAFORMA / RTLS",
"MISSION CONTROL · FIRST LAUNCH": "CENTRO DE MANDO · PRIMER INICIO",
"Choose your first view": "Elige tu primera vista",
@@ -1696,7 +1669,6 @@
"Observed history": "Historial observado",
"Later observation": "Observación posterior",
"Latest": "Último",
"news list|Latest": "Últimas",
"Newest frame per product": "Fotograma más reciente por producto",
"Replay observed history": "Reproducir el historial observado",
"S on another day replaces it · × unpins": "S en otro día lo reemplaza · × quita la fijación",
@@ -3501,11 +3473,7 @@
"Photo of {name}": "Foto de {name}",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": {
"one": "{count} avistamiento a la vista",
"many": "{count} de avistamientos a la vista",
"other": "{count} avistamientos a la vista"
},
"{count} sightings in view": "Avistamientos a la vista: {count}",
"Close sighting": "Cerrar avistamiento",
"Wildlife observed within 48 h (whales 7 d). Color and badge glyph show the taxon group. Sensitive species are shown only as 0.2° cells; Salish Sea marine mammal reports via Acartia are rounded to 0.01°. Not a census.": "Fauna y flora observadas en las últimas 48 h (cetáceos 7 días). El color y el símbolo de la insignia indican el grupo taxonómico. Las especies sensibles solo se muestran en celdas de 0,2°; los informes de mamíferos marinos de Acartia en el mar de Salish se redondean a 0,01°. No es un censo.",
"Open {name} on iNaturalist": "Abrir {name} en iNaturalist",
@@ -3516,31 +3484,20 @@
"{days}d": "{days} d",
"All markers": "Todos los marcadores",
"Show markers of every age": "Mostrar marcadores de cualquier antigüedad",
"Maximum event age: {days}d; undated markers are kept": {
"one": "Antigüedad máxima: {days} día; se conservan los marcadores sin fecha",
"many": "Antigüedad máxima: {days} de días; se conservan los marcadores sin fecha",
"other": "Antigüedad máxima: {days} días; se conservan los marcadores sin fecha"
},
"Maximum event age: {days}d; undated markers are kept": "Antigüedad máxima: {days} d; se conservan los marcadores sin fecha",
"event age filter|All": "Todos",
"Event age": "Antigüedad",
"Event age: {days}d · undated kept": {
"one": "Antigüedad: {days} día · se conservan los marcadores sin fecha",
"many": "Antigüedad: {days} de días · se conservan los marcadores sin fecha",
"other": "Antigüedad: {days} días · se conservan los marcadores sin fecha"
},
"Event age: {days}d · undated kept": "Antigüedad: {days} d · se conservan los marcadores sin fecha",
"Close camera preview": "Cerrar vista previa de la cámara",
"Refresh map still": "Actualizar imagen fija del mapa",
"The map still uses a separate request and may show a different moment.": "La imagen del mapa usa una solicitud independiente y puede mostrar otro momento.",
"Video plays from Québec 511, which sees your IP address.": "El vídeo se reproduce desde Québec 511, que ve tu dirección IP.",
"Press OPEN to load the photo": "Pulsa ABRIR para cargar la foto",
"Captured {month}": "Capturada en {month}",
"this spot": "este punto",
"the view centre": "el centro de la vista",
"{source} at {place}": "{source} en {place}",
"{source} at this spot": "{source} en este punto",
"{source} at the view centre": "{source} en el centro de la vista",
"Street level at {place}": "A pie de calle en {place}",
"Street level at this spot": "A pie de calle en este punto",
"Street level at the view centre": "A pie de calle en el centro de la vista",
"Switching map": "Cambiando de mapa",
"Opening imagery": "Abriendo imágenes",
"Street View is ready: press {open} to load it": "Street View está listo: pulsa {open} para cargarlo",
@@ -3632,7 +3589,6 @@
"{source} imagery could not be opened": "No se pudieron abrir las imágenes de {source}",
"{place} has no position to look for imagery at": "{place} no tiene una posición donde buscar imágenes",
"No {source} imagery near {place}": "No hay imágenes de {source} cerca de {place}",
"No {source} imagery near the view centre": "No hay imágenes de {source} cerca del centro de la vista",
"{reason} (filters on: {filters})": "{reason} (filtros activos: {filters})",
"tiles unavailable": "teselas no disponibles",
"Environment": "Medio ambiente",
@@ -3715,11 +3671,7 @@
"switch on a dataset to load features": "activa un conjunto de datos para cargar elementos",
"Data Library at a glance": "La biblioteca de datos de un vistazo",
"Map features": "Elementos del mapa",
"Showing {shown} of {total}": {
"one": "Mostrando {shown} de {total}",
"many": "Mostrando {shown} de {total}",
"other": "Mostrando {shown} de {total}"
},
"Showing {shown} of {total}": "Mostrando {shown} de {total}",
"Datasets": "Conjuntos de datos",
"Themes": "Temas",
"zoom in to load this layer": "acércate para cargar esta capa",
@@ -3740,5 +3692,12 @@
"500–1,000 m": "500–1000 m",
"1,000–2,000 m": "1000–2000 m",
"2,000–3,000 m": "2000–3000 m",
"3,000 m and above": "3000 m o más"
"3,000 m and above": "3000 m o más",
"The globe stopped drawing": "El globo dejó de dibujarse",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "El navegador retiró los gráficos 3D del globo. Las capas y los controles siguen funcionando, pero el mapa no se dibujará hasta que se recargue la página.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "El renderizado se detuvo tras un error. Las capas y los controles siguen funcionando, pero el mapa no se actualiza.",
"Reset view reloads the app without the view restored from the address bar.": "Restablecer la vista recarga la app sin la vista restaurada desde la barra de direcciones.",
"Reload": "Recargar",
"Retry": "Reintentar",
"Reset view": "Restablecer la vista"
}
+19 -60
View File
@@ -68,16 +68,8 @@
"{severity} only": "{severity} uniquement",
"All categories": "Toutes les catégories",
"Reading the news…": "Lecture des actualités…",
"News, last {hours} hours": {
"one": "Actualités de la dernière {hours} heure",
"many": "Actualités des dernières {hours} d'heures",
"other": "Actualités des dernières {hours} heures"
},
"Last {hours} h": {
"one": "Dernière {hours} h",
"many": "Dernières {hours} h",
"other": "Dernières {hours} h"
},
"News, last {hours} hours": "Actualités sur {hours} h",
"Last {hours} h": "Sur {hours} h",
"Time window": "Période",
"Conflicts": "Conflits",
"Local": "Local",
@@ -86,17 +78,9 @@
"Hotspots": "Foyers",
"Severity": "Gravité",
"Category": "Catégorie",
"Nothing reported near here in the last {hours} h": {
"one": "Rien de signalé près d'ici cette dernière {hours} h",
"many": "Rien de signalé près d'ici ces dernières {hours} h",
"other": "Rien de signalé près d'ici ces dernières {hours} h"
},
"Nothing reported near here in the last {hours} h": "Rien de signalé près d'ici en {hours} h",
"Latest news": "Dernières actualités",
"No news matches these filters in the last {hours} h": {
"one": "Aucune actualité de la dernière {hours} h ne correspond à ces filtres",
"many": "Aucune actualité des dernières {hours} h ne correspond à ces filtres",
"other": "Aucune actualité des dernières {hours} h ne correspond à ces filtres"
},
"No news matches these filters in the last {hours} h": "Aucune actualité ne correspond à ces filtres sur {hours} h",
"Load older news": "Charger des actualités plus anciennes",
"How to read the map": "Comment lire la carte",
"Colour: severity · icon: type of event": "Couleur : gravité · icône : type d'événement",
@@ -600,7 +584,6 @@
"Previous — prior visited contact in the 250 km window": "Précédent — contact visité précédemment dans la fenêtre de 250 km",
"PREVIOUS": "PRÉC.",
"FOCUS": "CENTRER",
"site status|FOCUS": "VISÉ",
"Next — nearest unvisited contact in the 250 km window": "Suivant — contact non visité le plus proche dans la fenêtre de 250 km",
"NEXT": "SUIV.",
"Named areas ({n})": "Zones nommées ({n})",
@@ -1849,11 +1832,7 @@
"ROUTE DATA UNAVAILABLE": "DONNÉES DE ROUTE INDISPONIBLES",
"CONTACT NAVIGATION OFF": "NAVIGATION DES CONTACTS DÉSACTIVÉE",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "LA PASSERELLE RESTE SUR LE NAVIRE SÉLECTIONNÉ",
"{range} NM": {
"one": "{range} NM",
"many": "{range} NM",
"other": "{range} NM"
},
"{range} NM": "{range} NM",
"AIS DESTINATION · CREW REPORTED": "DESTINATION AIS · DÉCLARÉE PAR L'ÉQUIPAGE",
"DEST {bearing}": "DEST {bearing}",
"Disable cockpit weather effects": "Désactiver les effets météo du cockpit",
@@ -1901,7 +1880,6 @@
"Reclassify as TR-3B": "Reclasser en TR-3B",
"TRACK ACQUIRED": "PISTE ACQUISE",
"{label} · COURSE {heading}°": "{label} · ROUTE {heading}°",
"vessel|{label} · COURSE {heading}°": "{label} · ROUTE {heading}°",
"{label} · HEADING {heading}°": "{label} · CAP {heading}°",
"{label} · NO HEADING": "{label} · SANS CAP",
"{label} · NADIR VIEW": "{label} · VUE AU NADIR",
@@ -2174,7 +2152,6 @@
"Observed history": "Historique des observations",
"Later observation": "Observation suivante",
"Latest": "Dernière",
"news list|Latest": "Dernières",
"Newest frame per product": "Image la plus récente par produit",
"Replay observed history": "Rejouer l'historique des observations",
"Imagery on Esri · Google 3D returns when cleared": "Sur Esri · EFFACER rétablit Google 3D",
@@ -2903,11 +2880,7 @@
"Contact cockpit summary": "Résumé cockpit du contact",
"CONTACT": "CONTACT",
"CONTACTS · 250 KM": "CONTACTS · 250 KM",
"RINGS {spacing} NM": {
"one": "CERCLES {spacing} NM",
"many": "CERCLES {spacing} NM",
"other": "CERCLES {spacing} NM"
},
"RINGS {spacing} NM": "CERCLES {spacing} NM",
"{count} CONTACTS": {
"one": "{count} CONTACT",
"many": "{count} DE CONTACTS",
@@ -3234,7 +3207,6 @@
"Left panels": "Panneaux de gauche",
"Right panels": "Panneaux de droite",
"CONFIRMED": "CONFIRMÉE",
"sightings filter|CONFIRMED": "CONFIRMÉES",
"PAD / RTLS": "PAS DE TIR / RTLS",
"fire confidence filter|All": "Toutes",
"fire confidence filter|Nominal+": "Nominale+",
@@ -3501,11 +3473,7 @@
"Photo of {name}": "Photo : {name}",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": {
"one": "{count} observation dans la vue",
"many": "{count} d'observations dans la vue",
"other": "{count} observations dans la vue"
},
"{count} sightings in view": "Observations dans la vue : {count}",
"Close sighting": "Fermer l'observation",
"Wildlife observed within 48 h (whales 7 d). Color and badge glyph show the taxon group. Sensitive species are shown only as 0.2° cells; Salish Sea marine mammal reports via Acartia are rounded to 0.01°. Not a census.": "Faune observée au cours des dernières 48 h (cétacés 7 jours). La couleur et le pictogramme du badge indiquent le groupe taxonomique. Les espèces sensibles sont limitées à des cellules de 0,2° ; les signalements de mammifères marins en mer des Salish provenant d'Acartia sont arrondis à 0,01°. Pas un recensement.",
"Open {name} on iNaturalist": "Ouvrir {name} sur iNaturalist",
@@ -3516,31 +3484,20 @@
"{days}d": "{days} j",
"All markers": "Tous les marqueurs",
"Show markers of every age": "Afficher les marqueurs de tout âge",
"Maximum event age: {days}d; undated markers are kept": {
"one": "Ancienneté maximale : {days} j ; marqueurs sans date conservés",
"many": "Ancienneté maximale : {days} j ; marqueurs sans date conservés",
"other": "Ancienneté maximale : {days} j ; marqueurs sans date conservés"
},
"Maximum event age: {days}d; undated markers are kept": "Ancienneté maximale : {days} j ; marqueurs sans date conservés",
"event age filter|All": "Tous",
"Event age": "Ancienneté",
"Event age: {days}d · undated kept": {
"one": "Ancienneté : {days} j · marqueurs sans date conservés",
"many": "Ancienneté : {days} j · marqueurs sans date conservés",
"other": "Ancienneté : {days} j · marqueurs sans date conservés"
},
"Event age: {days}d · undated kept": "Ancienneté : {days} j · marqueurs sans date conservés",
"Close camera preview": "Fermer l'aperçu de la caméra",
"Refresh map still": "Actualiser l'image fixe sur la carte",
"The map still uses a separate request and may show a different moment.": "L'image fixe de la carte utilise une requête distincte et peut montrer un autre moment.",
"Video plays from Québec 511, which sees your IP address.": "La vidéo est lue depuis Québec 511, qui voit votre adresse IP.",
"Press OPEN to load the photo": "Appuyez sur OUVRIR pour charger la photo",
"Captured {month}": "Prise en {month}",
"this spot": "cet endroit",
"the view centre": "le centre de la vue",
"{source} at {place}": "{source} : {place}",
"{source} at this spot": "{source} à cet endroit",
"{source} at the view centre": "{source} au centre de la vue",
"Street level at {place}": "Niveau rue : {place}",
"Street level at this spot": "Niveau rue à cet endroit",
"Street level at the view centre": "Niveau rue au centre de la vue",
"Switching map": "Changement de carte",
"Opening imagery": "Ouverture des images",
"Street View is ready: press {open} to load it": "Street View est prêt : appuyez sur {open} pour le charger",
@@ -3632,7 +3589,6 @@
"{source} imagery could not be opened": "Impossible d'ouvrir les images de {source}",
"{place} has no position to look for imagery at": "{place} n'a pas de position où chercher des images",
"No {source} imagery near {place}": "Aucune image {source} près de {place}",
"No {source} imagery near the view centre": "Aucune image {source} près du centre de la vue",
"{reason} (filters on: {filters})": "{reason} (filtres actifs : {filters})",
"tiles unavailable": "tuiles indisponibles",
"zoom in to load this layer": "zoomez pour charger cette couche",
@@ -3735,10 +3691,13 @@
"REMOVE ALL": "TOUT RETIRER",
"Confirm: remove every added entry from Data Layers": "Confirmer : retirer du panneau Couches toutes les entrées ajoutées",
"Remove every added entry from Data Layers": "Retirer du panneau Couches toutes les entrées ajoutées",
"Showing {shown} of {total}": {
"one": "{shown} sur {total} affichée",
"many": "{shown} sur {total} affichées",
"other": "{shown} sur {total} affichées"
},
"BROWSE DATA LIBRARY": "PARCOURIR LA BIBLIOTHÈQUE DE DONNÉES"
"Showing {shown} of {total}": "Affichées : {shown} sur {total}",
"BROWSE DATA LIBRARY": "PARCOURIR LA BIBLIOTHÈQUE DE DONNÉES",
"The globe stopped drawing": "Le globe ne s’affiche plus",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "Le navigateur a retiré au globe ses graphismes 3D. Les calques et les commandes fonctionnent toujours, mais la carte ne pourra s’afficher qu’après le rechargement de la page.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "Le rendu s’est arrêté après une erreur. Les calques et les commandes fonctionnent toujours, mais la carte ne se met plus à jour.",
"Reset view reloads the app without the view restored from the address bar.": "Réinitialiser la vue recharge l’app sans la vue restaurée depuis la barre d’adresse.",
"Reload": "Recharger",
"Retry": "Réessayer",
"Reset view": "Réinitialiser la vue"
}
+19 -60
View File
@@ -478,7 +478,6 @@
"PREVIOUS": "הקודם",
"Previous — prior visited contact in the 250 km window": "הקודם — מגע שכבר ביקרת בו בחלון של 250 ק\"מ",
"FOCUS": "התמקד",
"site status|FOCUS": "במוקד",
"NEXT": "הבא",
"Next — nearest unvisited contact in the 250 km window": "הבא — המגע הקרוב ביותר שלא ביקרת בו בחלון של 250 ק\"מ",
"Named areas ({n})": "אזורים בעלי שם ({n})",
@@ -833,7 +832,6 @@
"ORBIT REPLAY · {mission}": "שחזור מסלול · {mission}",
"PAUSED · {title}": "מושהה · {title}",
"CONFIRMED": "מאושר",
"sightings filter|CONFIRMED": "מאושרות",
"PAD / RTLS": "כן שיגור / RTLS",
"UNSPECIFIED": "לא צוין",
"{value} KM DOWNRANGE": "{value} ק\"מ לאורך המסלול",
@@ -1270,11 +1268,7 @@
"Wildlife observed within 48 h (whales 7 d). Color and badge glyph show the taxon group. Sensitive species are shown only as 0.2° cells; Salish Sea marine mammal reports via Acartia are rounded to 0.01°. Not a census.": "חיות בר נצפו ב-48 השעות האחרונות (לווייתנים · 7 ימים). הצבע וגליף התג מציגים את קבוצת הטקסון. מינים רגישים מוצגים כתאי 0.2° בלבד; דיווחי יונקי הים של Salish Sea דרך Acartia מעוגלים ל-0.01°. אינו מפקד אוכלוסין.",
"iNaturalist contributors · Acartia {count} (Salish Sea) · licences per record": "תורמי iNaturalist · Acartia {count} (Salish Sea) · רישיונות לכל רשומה",
"iNaturalist contributors · licences per record": "תורמי iNaturalist · רישיונות לכל רשומה",
"{count} sightings in view": {
"one": "{count} תצפית בתצוגה",
"two": "{count} תצפיות בתצוגה",
"other": "{count} תצפיות בתצוגה"
},
"{count} sightings in view": "{count} תצפיות בתצוגה",
"Zoom in for local sightings": "התקרב לתצפיות מקומיות",
"No recent sightings in view": "אין תצפיות אחרונות בתצוגה",
"Acartia feed unavailable": "פיד Acartia אינו זמין",
@@ -1743,7 +1737,6 @@
"SOURCE STATUS UNAVAILABLE": "מצב המקור אינו זמין",
"TRACK ACQUIRED": "המעקב ננעל",
"{label} · COURSE {heading}°": "{label} · מהלך {heading}°",
"vessel|{label} · COURSE {heading}°": "{label} · מהלך {heading}°",
"Context could not restore every layer; try again": "ההקשר לא שיחזר את כל השכבות; נסה שוב",
"No selected data layers": "אין שכבות נתונים נבחרות",
"{count} data layers could not be cleared": {
@@ -2005,7 +1998,6 @@
"Later observation": "תצפית מאוחרת",
"Newest frame per product": "השקופית החדשה לכל מוצר",
"Latest": "האחרונה",
"news list|Latest": "האחרונות",
"Replay observed history": "הצגת ההיסטוריה הנצפית שוב",
"Imagery on Esri · Google 3D returns when cleared": "צילומים ב-Esri · Google 3D חוזר כשמנקים",
"Point the camera at the ground to use the view": "כוון את המצלמה אל הקרקע כדי להשתמש בתצוגה",
@@ -3246,17 +3238,9 @@
"event age filter|All": "הכול",
"{days}d": "{days} ימ׳",
"Show markers of every age": "הצגת סמנים בכל גיל",
"Maximum event age: {days}d; undated markers are kept": {
"one": "גיל אירוע מרבי: {days} יום; סמנים ללא תאריך נשמרים",
"two": "גיל אירוע מרבי: {days} ימים; סמנים ללא תאריך נשמרים",
"other": "גיל אירוע מרבי: {days} ימים; סמנים ללא תאריך נשמרים"
},
"Maximum event age: {days}d; undated markers are kept": "גיל אירוע מרבי: {days} ימ׳; סמנים ללא תאריך נשמרים",
"All markers": "כל הסמנים",
"Event age: {days}d · undated kept": {
"one": "גיל האירוע: {days} יום · ללא תאריך נשמרים",
"two": "גיל האירוע: {days} ימים · ללא תאריך נשמרים",
"other": "גיל האירוע: {days} ימים · ללא תאריך נשמרים"
},
"Event age: {days}d · undated kept": "גיל האירוע: {days} ימ׳ · ללא תאריך נשמרים",
"30-DAY MISSION INDEX UNAVAILABLE": "רשימת המשימות ל-30 יום אינה זמינה",
"STATIONS": "תחנות",
"Crewed stations and the craft that visit them": "תחנות מאוישות והחלליות המבקרות בהן",
@@ -3292,11 +3276,7 @@
"LIVE AIS": "AIS חי",
"CONTACT NAVIGATION OFF": "ניווט המגעים כבוי",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "הגשר נשאר עם האונייה הנבחרת",
"{range} NM": {
"one": "{range} NM",
"two": "{range} NM",
"other": "{range} NM"
},
"{range} NM": "{range} NM",
"AIS DESTINATION · CREW REPORTED": "יעד AIS · דווח על ידי הצוות",
"FWD": "קדימה",
"AFT": "אחורה",
@@ -3315,13 +3295,10 @@
"Video plays from Québec 511, which sees your IP address.": "הווידאו זורם מ-Québec 511, שרואה את כתובת ה-IP שלך.",
"Press OPEN to load the photo": "לחץ על \"פתח\" כדי לטעון את התמונה",
"Captured {month}": "צולם ב-{month}",
"this spot": "המקום הזה",
"the view centre": "מרכז התצוגה",
"{source} at {place}": "{source}: {place}",
"{source} at this spot": "{source} במקום הזה",
"{source} at the view centre": "{source} במרכז התצוגה",
"Street level at {place}": "צילומי רחוב: {place}",
"Street level at this spot": "צילומי רחוב במקום הזה",
"Street level at the view centre": "צילומי רחוב במרכז התצוגה",
"Switching map": "מחליף מפה",
"Opening imagery": "פותח צילומים",
"Street View is ready: press {open} to load it": "Street View מוכן: לחץ על \"{open}\" כדי לטעון אותו",
@@ -3381,11 +3358,7 @@
"Zoom in (+)": "התקרבות (+)",
"Zoom to the imaging sensor swath": "זום אל רצועת הסריקה של חיישן הצילום",
"SENSOR": "חיישן",
"RINGS {spacing} NM": {
"one": "טבעות {spacing} NM",
"two": "טבעות {spacing} NM",
"other": "טבעות {spacing} NM"
},
"RINGS {spacing} NM": "טבעות {spacing} NM",
"{count} CONTACTS": {
"one": "{count} מגע",
"two": "{count} מגעים",
@@ -3531,36 +3504,20 @@
"{severity} and above": "{severity} ומעלה",
"{severity} only": "{severity} בלבד",
"All categories": "כל הקטגוריות",
"Last {hours} h": {
"one": "השעה האחרונה ({hours})",
"two": "{hours} שע׳ אחרונות",
"other": "{hours} שע׳ אחרונות"
},
"Last {hours} h": "{hours} שע׳ אחורה",
"News from {area}": "חדשות מ-{area}",
"Within {km} km of the view centre": "בטווח {km} ק\"מ ממרכז התצוגה",
"Reading the news…": "קורא את החדשות…",
"News, last {hours} hours": {
"one": "חדשות, השעה האחרונה ({hours})",
"two": "חדשות, {hours} השעות האחרונות",
"other": "חדשות, {hours} השעות האחרונות"
},
"News, last {hours} hours": "חדשות, {hours} שע׳ אחורה",
"Time window": "חלון זמן",
"News sections": "מדורי חדשות",
"Hotspots": "מוקדים",
"Severity": "חומרה",
"Category": "קטגוריה",
"Zoom in to see local news": "התקרב כדי לראות חדשות מקומיות",
"Nothing reported near here in the last {hours} h": {
"one": "לא דווח דבר בסביבה בשעה האחרונה ({hours})",
"two": "לא דווח דבר בסביבה ב־{hours} השעות האחרונות",
"other": "לא דווח דבר בסביבה ב־{hours} השעות האחרונות"
},
"Nothing reported near here in the last {hours} h": "לא דווח דבר בסביבה בחלון של {hours} שע׳",
"Latest news": "חדשות אחרונות",
"No news matches these filters in the last {hours} h": {
"one": "אין חדשות התואמות למסננים אלה בשעה האחרונה ({hours})",
"two": "אין חדשות התואמות למסננים אלה ב־{hours} השעות האחרונות",
"other": "אין חדשות התואמות למסננים אלה ב־{hours} השעות האחרונות"
},
"No news matches these filters in the last {hours} h": "אין חדשות התואמות למסננים אלה בחלון של {hours} שע׳",
"Load older news": "טעינת חדשות ישנות יותר",
"How to read the map": "איך לקרוא את המפה",
"Colour: severity · icon: type of event": "צבע: חומרה · סמל: סוג האירוע",
@@ -3632,7 +3589,6 @@
"{source} imagery could not be opened": "לא ניתן היה לפתוח את הצילומים של {source}",
"{place} has no position to look for imagery at": "ל-{place} אין מיקום לחיפוש צילומים",
"No {source} imagery near {place}": "אין צילומים של {source} ליד {place}",
"No {source} imagery near the view centre": "אין צילומים של {source} ליד מרכז התצוגה",
"{reason} (filters on: {filters})": "{reason} (מסננים פעילים: {filters})",
"tiles unavailable": "האריחים אינם זמינים",
"zoom in to load this layer": "התקרב כדי לטעון את השכבה הזאת",
@@ -3735,10 +3691,13 @@
"REMOVE ALL": "הסרת הכול",
"Confirm: remove every added entry from Data Layers": "אישור: הסרת כל הפריטים שנוספו משכבות נתונים",
"Remove every added entry from Data Layers": "הסרת כל הפריטים שנוספו משכבות נתונים",
"Showing {shown} of {total}": {
"one": "מוצג {shown} מתוך {total}",
"two": "מוצגים {shown} מתוך {total}",
"other": "מוצגים {shown} מתוך {total}"
},
"BROWSE DATA LIBRARY": "עיון בספריית הנתונים"
"Showing {shown} of {total}": "מוצגים {shown} מתוך {total}",
"BROWSE DATA LIBRARY": "עיון בספריית הנתונים",
"The globe stopped drawing": "הגלובוס הפסיק להצטייר",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "הדפדפן לקח בחזרה את הגרפיקה התלת-ממדית של הגלובוס. השכבות והפקדים עדיין פועלים, אבל המפה לא תוכל להצטייר עד שהעמוד ייטען מחדש.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "הרינדור נעצר בעקבות שגיאה. השכבות והפקדים עדיין פועלים, אבל המפה אינה מתעדכנת.",
"Reset view reloads the app without the view restored from the address bar.": "\"איפוס תצוגה\" טוען מחדש את האפליקציה בלי התצוגה ששוחזרה משורת הכתובת.",
"Reload": "טען מחדש",
"Retry": "נסה שוב",
"Reset view": "איפוס תצוגה"
}
+20 -61
View File
@@ -68,16 +68,8 @@
"{severity} only": "Solo {severity}",
"All categories": "Tutte le categorie",
"Reading the news…": "Lettura delle notizie…",
"News, last {hours} hours": {
"one": "Notizie dell'ultima {hours} ora",
"many": "Notizie delle ultime {hours} di ore",
"other": "Notizie delle ultime {hours} ore"
},
"Last {hours} h": {
"one": "Ultima {hours} h",
"many": "Ultime {hours} h",
"other": "Ultime {hours} h"
},
"News, last {hours} hours": "Notizie, periodo in ore: {hours}",
"Last {hours} h": "Periodo: {hours} h",
"Time window": "Periodo",
"Conflicts": "Conflitti",
"Local": "Locale",
@@ -86,17 +78,9 @@
"Hotspots": "Focolai",
"Severity": "Gravità",
"Category": "Categoria",
"Nothing reported near here in the last {hours} h": {
"one": "Nessuna segnalazione qui vicino nell'ultima {hours} h",
"many": "Nessuna segnalazione qui vicino nelle ultime {hours} h",
"other": "Nessuna segnalazione qui vicino nelle ultime {hours} h"
},
"Nothing reported near here in the last {hours} h": "Nessuna segnalazione qui vicino · periodo: {hours} h",
"Latest news": "Ultime notizie",
"No news matches these filters in the last {hours} h": {
"one": "Nessuna notizia dell'ultima {hours} h corrisponde a questi filtri",
"many": "Nessuna notizia delle ultime {hours} h corrisponde a questi filtri",
"other": "Nessuna notizia delle ultime {hours} h corrisponde a questi filtri"
},
"No news matches these filters in the last {hours} h": "Nessuna notizia corrisponde a questi filtri · periodo: {hours} h",
"Load older news": "Carica notizie precedenti",
"How to read the map": "Come leggere la mappa",
"Colour: severity · icon: type of event": "Colore: gravità · icona: tipo di evento",
@@ -600,7 +584,6 @@
"Previous — prior visited contact in the 250 km window": "Precedente — contatto visitato in precedenza nella finestra di 250 km",
"PREVIOUS": "PREC",
"FOCUS": "FOCUS",
"site status|FOCUS": "FOCUS",
"Next — nearest unvisited contact in the 250 km window": "Successivo — contatto non visitato più vicino nella finestra di 250 km",
"NEXT": "SUCC",
"Named areas ({n})": "Aree con nome ({n})",
@@ -958,7 +941,7 @@
"ORBITAL VELOCITY": "VELOCITÀ ORBITALE",
"SUB-SATELLITE POINT": "PUNTO SUBSATELLITARE",
"NORTH UP": "NORD IN ALTO",
"CURRENTLY OVER": "SOTTO IL SATELLITE",
"CURRENTLY OVER": "ATTUALMENTE SOPRA",
"{division}, {country}": "{division}, {country}",
"{region} · near {place}": "{region} · vicino a {place}",
"NORAD {id}": "NORAD {id}",
@@ -1849,11 +1832,7 @@
"ROUTE DATA UNAVAILABLE": "DATI ROTTA NON DISPONIBILI",
"CONTACT NAVIGATION OFF": "NAVIGAZIONE CONTATTI DISATTIVATA",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "LA PLANCIA RESTA SULLA NAVE SELEZIONATA",
"{range} NM": {
"one": "{range} MN",
"many": "{range} MN",
"other": "{range} MN"
},
"{range} NM": "{range} MN",
"AIS DESTINATION · CREW REPORTED": "DESTINAZIONE AIS · DICHIARATA DALL'EQUIPAGGIO",
"DEST {bearing}": "DEST {bearing}",
"Disable cockpit weather effects": "Disattiva gli effetti meteo del cockpit",
@@ -1901,7 +1880,6 @@
"Reclassify as TR-3B": "Riclassifica come TR-3B",
"TRACK ACQUIRED": "TRACCIA ACQUISITA",
"{label} · COURSE {heading}°": "{label} · ROTTA {heading}°",
"vessel|{label} · COURSE {heading}°": "{label} · ROTTA {heading}°",
"{label} · HEADING {heading}°": "{label} · PRUA {heading}°",
"{label} · NO HEADING": "{label} · PRUA N/D",
"{label} · NADIR VIEW": "{label} · VISTA AL NADIR",
@@ -2174,7 +2152,6 @@
"Observed history": "Storico osservato",
"Later observation": "Osservazione successiva",
"Latest": "Più recente",
"news list|Latest": "Più recenti",
"Newest frame per product": "Fotogramma più recente per prodotto",
"Replay observed history": "Rivedi lo storico osservato",
"Imagery on Esri · Google 3D returns when cleared": "Immagini su Esri · Google 3D torna con CANCELLA",
@@ -2903,11 +2880,7 @@
"Contact cockpit summary": "Riepilogo contatto del cockpit",
"CONTACT": "CONTATTO",
"CONTACTS · 250 KM": "CONTATTI · 250 KM",
"RINGS {spacing} NM": {
"one": "CERCHI {spacing} MN",
"many": "CERCHI {spacing} MN",
"other": "CERCHI {spacing} MN"
},
"RINGS {spacing} NM": "CERCHI {spacing} MN",
"{count} CONTACTS": {
"one": "{count} CONTATTO",
"many": "{count} DI CONTATTI",
@@ -3234,7 +3207,6 @@
"Left panels": "Pannelli a sinistra",
"Right panels": "Pannelli a destra",
"CONFIRMED": "CONFERMATA",
"sightings filter|CONFIRMED": "CONFERMATE",
"PAD / RTLS": "RAMPA / RTLS",
"fire confidence filter|All": "Tutti",
"fire confidence filter|Nominal+": "Nominale+",
@@ -3501,11 +3473,7 @@
"Photo of {name}": "Foto di {name}",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": {
"one": "{count} avvistamento nella vista",
"many": "{count} di avvistamenti nella vista",
"other": "{count} avvistamenti nella vista"
},
"{count} sightings in view": "Avvistamenti nella vista: {count}",
"Close sighting": "Chiudi avvistamento",
"Wildlife observed within 48 h (whales 7 d). Color and badge glyph show the taxon group. Sensitive species are shown only as 0.2° cells; Salish Sea marine mammal reports via Acartia are rounded to 0.01°. Not a census.": "Fauna osservata nelle ultime 48 h (cetacei 7 giorni). Colore e simbolo del contrassegno indicano il gruppo tassonomico. Le specie sensibili sono mostrate solo in celle di 0,2°; le segnalazioni di mammiferi marini di Acartia nel mar dei Salish sono arrotondate a 0,01°. Non è un censimento.",
"Open {name} on iNaturalist": "Apri {name} su iNaturalist",
@@ -3516,31 +3484,20 @@
"{days}d": "{days} g",
"All markers": "Tutti i marcatori",
"Show markers of every age": "Mostra i marcatori di qualsiasi età",
"Maximum event age: {days}d; undated markers are kept": {
"one": "Età massima degli eventi: {days} giorno; marcatori senza data mantenuti",
"many": "Età massima degli eventi: {days} di giorni; marcatori senza data mantenuti",
"other": "Età massima degli eventi: {days} giorni; marcatori senza data mantenuti"
},
"Maximum event age: {days}d; undated markers are kept": "Età massima degli eventi: {days} g; marcatori senza data mantenuti",
"event age filter|All": "Tutti",
"Event age": "Età degli eventi",
"Event age: {days}d · undated kept": {
"one": "Età degli eventi: {days} giorno · senza data mantenuti",
"many": "Età degli eventi: {days} di giorni · senza data mantenuti",
"other": "Età degli eventi: {days} giorni · senza data mantenuti"
},
"Event age: {days}d · undated kept": "Età degli eventi: {days} g · senza data mantenuti",
"Close camera preview": "Chiudi anteprima della telecamera",
"Refresh map still": "Aggiorna immagine fissa sulla mappa",
"The map still uses a separate request and may show a different moment.": "L'immagine sulla mappa usa una richiesta separata e può mostrare un momento diverso.",
"Video plays from Québec 511, which sees your IP address.": "Il video viene riprodotto da Québec 511, che vede il tuo indirizzo IP.",
"Press OPEN to load the photo": "Premi APRI per caricare la foto",
"Captured {month}": "Scattata: {month}",
"this spot": "questo punto",
"the view centre": "il centro della vista",
"{source} at {place}": "{source}: {place}",
"{source} at this spot": "{source} in questo punto",
"{source} at the view centre": "{source} al centro della vista",
"Street level at {place}": "Vista stradale: {place}",
"Street level at this spot": "Vista stradale in questo punto",
"Street level at the view centre": "Vista stradale al centro della vista",
"Switching map": "Cambio mappa",
"Opening imagery": "Apertura delle immagini",
"Street View is ready: press {open} to load it": "Street View è pronto: premi {open} per caricarlo",
@@ -3632,7 +3589,6 @@
"{source} imagery could not be opened": "Impossibile aprire le immagini di {source}",
"{place} has no position to look for imagery at": "{place} non ha una posizione in cui cercare immagini",
"No {source} imagery near {place}": "Nessuna immagine di {source} vicino a {place}",
"No {source} imagery near the view centre": "Nessuna immagine di {source} vicino al centro della vista",
"{reason} (filters on: {filters})": "{reason} (filtri attivi: {filters})",
"tiles unavailable": "tile non disponibili",
"zoom in to load this layer": "ingrandisci per caricare questo livello",
@@ -3735,10 +3691,13 @@
"REMOVE ALL": "RIMUOVI TUTTO",
"Confirm: remove every added entry from Data Layers": "Conferma: rimuovi da Livelli dati tutte le voci aggiunte",
"Remove every added entry from Data Layers": "Rimuovi da Livelli dati tutte le voci aggiunte",
"Showing {shown} of {total}": {
"one": "{shown} di {total} mostrata",
"many": "{shown} di {total} mostrate",
"other": "{shown} di {total} mostrate"
},
"BROWSE DATA LIBRARY": "ESPLORA LIBRERIA DATI"
"Showing {shown} of {total}": "Mostrati: {shown} su {total}",
"BROWSE DATA LIBRARY": "ESPLORA LIBRERIA DATI",
"The globe stopped drawing": "Il globo ha smesso di disegnare",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "Il browser ha ritirato la grafica 3D del globo. Livelli e controlli funzionano ancora, ma la mappa non può essere disegnata finché la pagina non viene ricaricata.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "Il rendering si è fermato dopo un errore. Livelli e controlli funzionano ancora, ma la mappa non si aggiorna.",
"Reset view reloads the app without the view restored from the address bar.": "Ripristina vista ricarica l’app senza la vista ripristinata dalla barra degli indirizzi.",
"Reload": "Ricarica",
"Retry": "Riprova",
"Reset view": "Ripristina vista"
}
+19 -40
View File
@@ -62,12 +62,8 @@
"{severity} only": "{severity}のみ",
"All categories": "すべてのカテゴリ",
"Reading the news…": "ニュースを読み込み中…",
"News, last {hours} hours": {
"other": "直近{hours}時間のニュース"
},
"Last {hours} h": {
"other": "直近{hours}時間"
},
"News, last {hours} hours": "直近{hours}時間のニュース",
"Last {hours} h": "直近{hours}時間",
"Time window": "期間",
"Conflicts": "紛争",
"Local": "ローカル",
@@ -76,13 +72,9 @@
"Hotspots": "ホットスポット",
"Severity": "深刻度",
"Category": "カテゴリ",
"Nothing reported near here in the last {hours} h": {
"other": "直近{hours}時間、この付近の報道はありません"
},
"Nothing reported near here in the last {hours} h": "直近{hours}時間、この付近の報道はありません",
"Latest news": "最新ニュース",
"No news matches these filters in the last {hours} h": {
"other": "直近{hours}時間で条件に合うニュースはありません"
},
"No news matches these filters in the last {hours} h": "直近{hours}時間で条件に合うニュースはありません",
"Load older news": "以前のニュースを読み込む",
"How to read the map": "地図の見方",
"Colour: severity · icon: type of event": "色:深刻度 · アイコン:出来事の種類",
@@ -572,7 +564,6 @@
"Previous — prior visited contact in the 250 km window": "前へ:250 km圏内で直前に訪れたコンタクト",
"PREVIOUS": "前へ",
"FOCUS": "フォーカス",
"site status|FOCUS": "フォーカス",
"Next — nearest unvisited contact in the 250 km window": "次へ:250 km圏内で最も近い未訪問のコンタクト",
"NEXT": "次へ",
"Named areas ({n})": "名前付きエリア({n})",
@@ -1785,9 +1776,7 @@
"ROUTE DATA UNAVAILABLE": "経路データなし",
"CONTACT NAVIGATION OFF": "コンタクトナビゲーション オフ",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "ブリッジは選択中の船舶に固定",
"{range} NM": {
"other": "{range}海里"
},
"{range} NM": "{range}海里",
"AIS DESTINATION · CREW REPORTED": "AIS目的地 · 乗組員入力",
"DEST {bearing}": "目的地 {bearing}",
"Disable cockpit weather effects": "コックピットの天候エフェクトを無効にする",
@@ -1833,7 +1822,6 @@
"Reclassify as TR-3B": "TR-3Bに再分類",
"TRACK ACQUIRED": "追跡開始",
"{label} · COURSE {heading}°": "{label} · 対地針路 {heading}°",
"vessel|{label} · COURSE {heading}°": "{label} · 対地針路 {heading}°",
"{label} · HEADING {heading}°": "{label} · 船首方位 {heading}°",
"{label} · NO HEADING": "{label} · 船首方位なし",
"{label} · NADIR VIEW": "{label} · 直下視",
@@ -2090,7 +2078,6 @@
"Observed history": "観測履歴",
"Later observation": "次の観測",
"Latest": "最新",
"news list|Latest": "最新",
"Newest frame per product": "プロダクトごとの最新フレーム",
"Replay observed history": "観測履歴を再生",
"Imagery on Esri · Google 3D returns when cleared": "画像はEsri上に表示 · 消去するとGoogle 3Dに戻ります",
@@ -2743,9 +2730,7 @@
"Contact cockpit summary": "コンタクトのコックピット概要",
"CONTACT": "コンタクト",
"CONTACTS · 250 KM": "コンタクト · 250 km",
"RINGS {spacing} NM": {
"other": "距離環 {spacing}海里"
},
"RINGS {spacing} NM": "距離環 {spacing}海里",
"{count} CONTACTS": {
"other": "コンタクト {count}件"
},
@@ -3072,7 +3057,6 @@
"Left panels": "左パネル",
"Right panels": "右パネル",
"CONFIRMED": "確認済み",
"sightings filter|CONFIRMED": "確認済み",
"PAD / RTLS": "射場 / RTLS",
"fire confidence filter|All": "すべて",
"fire confidence filter|Nominal+": "中以上",
@@ -3327,9 +3311,7 @@
"Photo of {name}": "{name}の写真",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": {
"other": "表示範囲に{count}件の目撃情報"
},
"{count} sightings in view": "表示範囲に{count}件の目撃情報",
"Close sighting": "目撃情報を閉じる",
"Wildlife observed within 48 h (whales 7 d). Color and badge glyph show the taxon group. Sensitive species are shown only as 0.2° cells; Salish Sea marine mammal reports via Acartia are rounded to 0.01°. Not a census.": "過去48時間以内に観察された野生生物(クジラ類は7日間)。色とバッジのアイコンは分類群を表します。保護が必要な種は0.2°のセルとしてのみ表示し、セイリッシュ海のAcartiaによる海生哺乳類の目撃情報は0.01°に丸めます。個体数調査ではありません。",
"Open {name} on iNaturalist": "iNaturalistで{name}を開く",
@@ -3340,27 +3322,20 @@
"{days}d": "{days}日",
"All markers": "すべてのマーカー",
"Show markers of every age": "日付にかかわらずすべてのマーカーを表示",
"Maximum event age: {days}d; undated markers are kept": {
"other": "イベントの最大経過日数:{days}日。日付のないマーカーは保持"
},
"Maximum event age: {days}d; undated markers are kept": "イベントの最大経過日数:{days}日。日付のないマーカーは保持",
"event age filter|All": "すべて",
"Event age": "経過日数",
"Event age: {days}d · undated kept": {
"other": "経過日数:{days}日以内 · 日付なしは保持"
},
"Event age: {days}d · undated kept": "経過日数:{days}日以内 · 日付なしは保持",
"Close camera preview": "カメラのプレビューを閉じる",
"Refresh map still": "地図上の静止画を更新",
"The map still uses a separate request and may show a different moment.": "地図上の静止画は別のリクエストで取得されるため、異なる時点を表示する場合があります。",
"Video plays from Québec 511, which sees your IP address.": "動画はQuébec 511から再生され、お使いのIPアドレスはQuébec 511側から見えます。",
"Press OPEN to load the photo": "[開く]を押して写真を読み込む",
"Captured {month}": "{month}撮影",
"this spot": "この地点",
"the view centre": "表示中心",
"{source} at {place}": "{place}の{source}",
"{source} at this spot": "この地点の{source}",
"{source} at the view centre": "表示中心の{source}",
"Street level at {place}": "{place}のストリートレベル",
"Street level at this spot": "この地点のストリートレベル",
"Street level at the view centre": "表示中心のストリートレベル",
"Switching map": "地図を切り替え中",
"Opening imagery": "画像を開いています",
"Street View is ready: press {open} to load it": "ストリートビューの準備ができました:[{open}]を押して読み込んでください",
@@ -3452,7 +3427,6 @@
"{source} imagery could not be opened": "{source}の画像を開けませんでした",
"{place} has no position to look for imagery at": "{place}には画像を探す位置がありません",
"No {source} imagery near {place}": "{place}の近くに{source}の画像はありません",
"No {source} imagery near the view centre": "表示中心の近くに{source}の画像はありません",
"{reason} (filters on: {filters})": "{reason}(有効なフィルター:{filters})",
"tiles unavailable": "タイルを利用できません",
"zoom in to load this layer": "ズームインしてこのレイヤーを読み込む",
@@ -3543,8 +3517,13 @@
"REMOVE ALL": "すべて削除",
"Confirm: remove every added entry from Data Layers": "確認:追加したすべての項目をデータレイヤーから削除",
"Remove every added entry from Data Layers": "追加したすべての項目をデータレイヤーから削除",
"Showing {shown} of {total}": {
"other": "{total}件中{shown}件を表示"
},
"BROWSE DATA LIBRARY": "データライブラリを見る"
"Showing {shown} of {total}": "{total}件中{shown}件を表示",
"BROWSE DATA LIBRARY": "データライブラリを見る",
"The globe stopped drawing": "地球儀の描画が止まりました",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "ブラウザーが地球儀の 3D グラフィックスを解放しました。レイヤーと操作は引き続き使えますが、ページを再読み込みするまで地図は描画されません。",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "エラーのため描画が停止しました。レイヤーと操作は引き続き使えますが、地図は更新されません。",
"Reset view reloads the app without the view restored from the address bar.": "「ビューをリセット」は、アドレスバーから復元したビューを使わずにアプリを再読み込みします。",
"Reload": "再読み込み",
"Retry": "再試行",
"Reset view": "ビューをリセット"
}
+21 -62
View File
@@ -68,16 +68,8 @@
"{severity} only": "Apenas {severity}",
"All categories": "Todas as categorias",
"Reading the news…": "Lendo as notícias…",
"News, last {hours} hours": {
"one": "Notícias da última {hours} hora",
"many": "Notícias das últimas {hours} de horas",
"other": "Notícias das últimas {hours} horas"
},
"Last {hours} h": {
"one": "Última {hours} h",
"many": "Últimas {hours} h",
"other": "Últimas {hours} h"
},
"News, last {hours} hours": "Notícias de até {hours} h atrás",
"Last {hours} h": "Até {hours} h atrás",
"Time window": "Período",
"Conflicts": "Conflitos",
"Local": "Local",
@@ -86,17 +78,9 @@
"Hotspots": "Focos",
"Severity": "Gravidade",
"Category": "Categoria",
"Nothing reported near here in the last {hours} h": {
"one": "Nada noticiado aqui perto na última {hours} h",
"many": "Nada noticiado aqui perto nas últimas {hours} h",
"other": "Nada noticiado aqui perto nas últimas {hours} h"
},
"Nothing reported near here in the last {hours} h": "Nada noticiado aqui perto desde {hours} h atrás",
"Latest news": "Últimas notícias",
"No news matches these filters in the last {hours} h": {
"one": "Nenhuma notícia da última {hours} h corresponde a estes filtros",
"many": "Nenhuma notícia das últimas {hours} h corresponde a estes filtros",
"other": "Nenhuma notícia das últimas {hours} h corresponde a estes filtros"
},
"No news matches these filters in the last {hours} h": "Nenhuma notícia de até {hours} h atrás corresponde a estes filtros",
"Load older news": "Carregar notícias anteriores",
"How to read the map": "Como ler o mapa",
"Colour: severity · icon: type of event": "Cor: gravidade · ícone: tipo de evento",
@@ -600,7 +584,6 @@
"Previous — prior visited contact in the 250 km window": "Anterior — contato visitado anteriormente na janela de 250 km",
"PREVIOUS": "ANT.",
"FOCUS": "FOCAR",
"site status|FOCUS": "EM FOCO",
"Next — nearest unvisited contact in the 250 km window": "Próximo — contato não visitado mais próximo na janela de 250 km",
"NEXT": "PRÓX.",
"Named areas ({n})": "Áreas com nome ({n})",
@@ -956,7 +939,7 @@
"ORBITAL VELOCITY": "VELOCIDADE ORBITAL",
"SUB-SATELLITE POINT": "PONTO SUBSATÉLITE",
"NORTH UP": "NORTE PARA CIMA",
"CURRENTLY OVER": "SOB O SATÉLITE",
"CURRENTLY OVER": "SOBREVOANDO AGORA",
"{division}, {country}": "{division}, {country}",
"{region} · near {place}": "{region} · perto de {place}",
"NORAD {id}": "NORAD {id}",
@@ -1849,11 +1832,7 @@
"ROUTE DATA UNAVAILABLE": "DADOS DE ROTA INDISPONÍVEIS",
"CONTACT NAVIGATION OFF": "NAVEGAÇÃO DE CONTATOS DESLIGADA",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "O PASSADIÇO PERMANECE NA EMBARCAÇÃO SELECIONADA",
"{range} NM": {
"one": "{range} MN",
"many": "{range} MN",
"other": "{range} MN"
},
"{range} NM": "{range} MN",
"AIS DESTINATION · CREW REPORTED": "DESTINO AIS · INFORMADO PELA TRIPULAÇÃO",
"DEST {bearing}": "DEST {bearing}",
"Disable cockpit weather effects": "Desativar efeitos de clima na cabine",
@@ -1900,8 +1879,7 @@
"Restore real aircraft": "Restaurar a aeronave real",
"Reclassify as TR-3B": "Reclassificar como TR-3B",
"TRACK ACQUIRED": "RASTRO ADQUIRIDO",
"{label} · COURSE {heading}°": "{label} · CURSO {heading}°",
"vessel|{label} · COURSE {heading}°": "{label} · RUMO {heading}°",
"{label} · COURSE {heading}°": "{label} · RUMO {heading}°",
"{label} · HEADING {heading}°": "{label} · PROA {heading}°",
"{label} · NO HEADING": "{label} · SEM PROA",
"{label} · NADIR VIEW": "{label} · VISTA AO NADIR",
@@ -2174,7 +2152,6 @@
"Observed history": "Histórico observado",
"Later observation": "Observação posterior",
"Latest": "Mais recente",
"news list|Latest": "Mais recentes",
"Newest frame per product": "Quadro mais recente por produto",
"Replay observed history": "Reproduzir o histórico observado",
"Imagery on Esri · Google 3D returns when cleared": "Imagens sobre o Esri · LIMPAR restaura o Google 3D",
@@ -2905,11 +2882,7 @@
"Contact cockpit summary": "Resumo do contato na cabine",
"CONTACT": "CONTATO",
"CONTACTS · 250 KM": "CONTATOS · 250 KM",
"RINGS {spacing} NM": {
"one": "ANÉIS {spacing} MN",
"many": "ANÉIS {spacing} MN",
"other": "ANÉIS {spacing} MN"
},
"RINGS {spacing} NM": "ANÉIS {spacing} MN",
"{count} CONTACTS": {
"one": "{count} CONTATO",
"many": "{count} DE CONTATOS",
@@ -3234,7 +3207,6 @@
"Left panels": "Painéis à esquerda",
"Right panels": "Painéis à direita",
"CONFIRMED": "CONFIRMADA",
"sightings filter|CONFIRMED": "CONFIRMADOS",
"PAD / RTLS": "PLATAFORMA / RTLS",
"fire confidence filter|All": "Todas",
"fire confidence filter|Nominal+": "Nominal+",
@@ -3501,11 +3473,7 @@
"Photo of {name}": "Foto de {name}",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": {
"one": "{count} avistamento na vista",
"many": "{count} de avistamentos na vista",
"other": "{count} avistamentos na vista"
},
"{count} sightings in view": "Avistamentos na vista: {count}",
"Close sighting": "Fechar avistamento",
"Wildlife observed within 48 h (whales 7 d). Color and badge glyph show the taxon group. Sensitive species are shown only as 0.2° cells; Salish Sea marine mammal reports via Acartia are rounded to 0.01°. Not a census.": "Vida selvagem observada nas últimas 48 h (cetáceos 7 dias). A cor e o símbolo do emblema indicam o grupo taxonômico. Espécies sensíveis aparecem apenas em células de 0,2°; os relatos de mamíferos marinhos do mar de Salish via Acartia são arredondados para 0,01°. Não é um censo.",
"Open {name} on iNaturalist": "Abrir {name} no iNaturalist",
@@ -3516,31 +3484,20 @@
"{days}d": "{days} d",
"All markers": "Todos os marcadores",
"Show markers of every age": "Mostrar marcadores de qualquer idade",
"Maximum event age: {days}d; undated markers are kept": {
"one": "Idade máxima do evento: {days} dia; marcadores sem data são mantidos",
"many": "Idade máxima do evento: {days} de dias; marcadores sem data são mantidos",
"other": "Idade máxima do evento: {days} dias; marcadores sem data são mantidos"
},
"Maximum event age: {days}d; undated markers are kept": "Idade máxima do evento: {days} d; marcadores sem data são mantidos",
"event age filter|All": "Todos",
"Event age": "Idade do evento",
"Event age: {days}d · undated kept": {
"one": "Idade do evento: {days} dia · sem data mantidos",
"many": "Idade do evento: {days} de dias · sem data mantidos",
"other": "Idade do evento: {days} dias · sem data mantidos"
},
"Event age: {days}d · undated kept": "Idade do evento: {days} d · sem data mantidos",
"Close camera preview": "Fechar prévia da câmera",
"Refresh map still": "Atualizar imagem fixa no mapa",
"The map still uses a separate request and may show a different moment.": "A imagem no mapa usa uma solicitação separada e pode mostrar outro momento.",
"Video plays from Québec 511, which sees your IP address.": "O vídeo é reproduzido a partir do Québec 511, que vê o seu endereço IP.",
"Press OPEN to load the photo": "Clique em ABRIR para carregar a foto",
"Captured {month}": "Capturada em {month}",
"this spot": "este ponto",
"the view centre": "o centro da vista",
"{source} at {place}": "{source}: {place}",
"{source} at this spot": "{source} neste ponto",
"{source} at the view centre": "{source} no centro da vista",
"Street level at {place}": "Nível da rua: {place}",
"Street level at this spot": "Nível da rua neste ponto",
"Street level at the view centre": "Nível da rua no centro da vista",
"Switching map": "Mudando de mapa",
"Opening imagery": "Abrindo imagens",
"Street View is ready: press {open} to load it": "O Street View está pronto: clique em {open} para carregar",
@@ -3632,7 +3589,6 @@
"{source} imagery could not be opened": "Não foi possível abrir as imagens de {source}",
"{place} has no position to look for imagery at": "{place} não tem uma posição para buscar imagens",
"No {source} imagery near {place}": "Nenhuma imagem de {source} perto de {place}",
"No {source} imagery near the view centre": "Nenhuma imagem de {source} perto do centro da vista",
"{reason} (filters on: {filters})": "{reason} (filtros ativos: {filters})",
"tiles unavailable": "blocos indisponíveis",
"zoom in to load this layer": "aproxime o zoom para carregar esta camada",
@@ -3735,10 +3691,13 @@
"REMOVE ALL": "REMOVER TUDO",
"Confirm: remove every added entry from Data Layers": "Confirmar: remover de \"Camadas de dados\" todas as entradas adicionadas",
"Remove every added entry from Data Layers": "Remover de \"Camadas de dados\" todas as entradas adicionadas",
"Showing {shown} of {total}": {
"one": "Mostrando {shown} de {total}",
"many": "Mostrando {shown} de {total}",
"other": "Mostrando {shown} de {total}"
},
"BROWSE DATA LIBRARY": "EXPLORAR BIBLIOTECA DE DADOS"
"Showing {shown} of {total}": "Mostrando {shown} de {total}",
"BROWSE DATA LIBRARY": "EXPLORAR BIBLIOTECA DE DADOS",
"The globe stopped drawing": "O globo parou de desenhar",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "O navegador retirou os gráficos 3D do globo. As camadas e os controles continuam funcionando, mas o mapa só volta a ser desenhado depois de recarregar a página.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "A renderização parou após um erro. As camadas e os controles continuam funcionando, mas o mapa não está sendo atualizado.",
"Reset view reloads the app without the view restored from the address bar.": "Redefinir vista recarrega o app sem a vista restaurada da barra de endereços.",
"Reload": "Recarregar",
"Retry": "Tentar novamente",
"Reset view": "Redefinir vista"
}
+19 -70
View File
@@ -71,18 +71,8 @@
"{severity} only": "Только: {severity}",
"All categories": "Все категории",
"Reading the news…": "Читаем новости…",
"News, last {hours} hours": {
"one": "Новости за последний {hours} час",
"few": "Новости за последние {hours} часа",
"many": "Новости за последние {hours} часов",
"other": "Новости за последние {hours} часа"
},
"Last {hours} h": {
"one": "За {hours} ч",
"few": "За {hours} ч",
"many": "За {hours} ч",
"other": "За {hours} ч"
},
"News, last {hours} hours": "Новости за последние {hours} ч",
"Last {hours} h": "За {hours} ч",
"Time window": "Период",
"Conflicts": "Конфликты",
"Local": "Местные",
@@ -91,19 +81,9 @@
"Hotspots": "Горячие точки",
"Severity": "Тяжесть",
"Category": "Категория",
"Nothing reported near here in the last {hours} h": {
"one": "За последний {hours} ч поблизости ничего не сообщалось",
"few": "За последние {hours} ч поблизости ничего не сообщалось",
"many": "За последние {hours} ч поблизости ничего не сообщалось",
"other": "За последние {hours} ч поблизости ничего не сообщалось"
},
"Nothing reported near here in the last {hours} h": "За последние {hours} ч поблизости ничего не сообщалось",
"Latest news": "Последние новости",
"No news matches these filters in the last {hours} h": {
"one": "За последний {hours} ч нет новостей по этим фильтрам",
"few": "За последние {hours} ч нет новостей по этим фильтрам",
"many": "За последние {hours} ч нет новостей по этим фильтрам",
"other": "За последние {hours} ч нет новостей по этим фильтрам"
},
"No news matches these filters in the last {hours} h": "За последние {hours} ч нет новостей по этим фильтрам",
"Load older news": "Загрузить более ранние новости",
"How to read the map": "Как читать карту",
"Colour: severity · icon: type of event": "Цвет: тяжесть · значок: тип события",
@@ -199,7 +179,6 @@
"Previous — prior visited contact in the 250 km window": "Назад — предыдущий посещённый контакт в радиусе 250 км",
"PREVIOUS": "ПРЕД.",
"FOCUS": "ФОКУС",
"site status|FOCUS": "ФОКУС",
"Next — nearest unvisited contact in the 250 km window": "Далее — ближайший непосещённый контакт в радиусе 250 км",
"NEXT": "СЛЕД.",
"Open-source mapped/observed context. Missing broadcasts, unloaded map areas, or unmapped sites are not evidence of absence.": "Контекст из открытых источников. Отсутствие трансляций, незагруженные области карты или неотмеченные объекты — не доказательство отсутствия.",
@@ -367,7 +346,6 @@
"Cancel replay": "Отменить повтор",
"EST. DOWNRANGE": "РАСЧЁТ ПО ТРАССЕ",
"CONFIRMED": "ПОДТВЕРЖДЕНО",
"sightings filter|CONFIRMED": "ПОДТВЕРЖДЕНО",
"PAD / RTLS": "ПЛОЩАДКА / RTLS",
"PLANNED · ": "ПЛАН · ",
"SUPPLIED TRAJECTORY POINTS": "ПРЕДОСТАВЛЕННЫЕ ТОЧКИ ТРАЕКТОРИИ",
@@ -567,12 +545,7 @@
"ROUTE DATA UNAVAILABLE": "ДАННЫЕ МАРШРУТА НЕДОСТУПНЫ",
"CONTACT NAVIGATION OFF": "НАВИГАЦИЯ ПО КОНТАКТАМ ВЫКЛЮЧЕНА",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "МОСТИК ОСТАЁТСЯ НА ВЫБРАННОМ СУДНЕ",
"{range} NM": {
"one": "{range} МОР. МИЛЯ",
"few": "{range} МОР. МИЛИ",
"many": "{range} МОР. МИЛЬ",
"other": "{range} МОР. МИЛИ"
},
"{range} NM": "{range} МОР. МИЛ.",
"AIS DESTINATION · CREW REPORTED": "ПУНКТ НАЗНАЧЕНИЯ AIS · УКАЗАН ЭКИПАЖЕМ",
"DEST {bearing}": "НАЗН. {bearing}",
"Disable cockpit weather effects": "Выключить погодные эффекты кабины",
@@ -618,7 +591,6 @@
"Select flight {title}": "Выбрать рейс {title}",
"TRACK ACQUIRED": "ЦЕЛЬ ЗАХВАЧЕНА",
"{label} · COURSE {heading}°": "{label} · ПУТЕВОЙ УГОЛ {heading}°",
"vessel|{label} · COURSE {heading}°": "{label} · ПУТЕВОЙ УГОЛ {heading}°",
"{label} · HEADING {heading}°": "{label} · КУРС {heading}°",
"{label} · NO HEADING": "{label} · НЕТ КУРСА",
"{label} · NADIR VIEW": "{label} · ВИД В НАДИР",
@@ -870,12 +842,7 @@
"EXIT BRIDGE": "ВЫЙТИ С МОСТИКА",
"EXIT ORBIT": "ВЫЙТИ С ОРБИТЫ",
"CONTACTS · 250 KM": "КОНТАКТЫ · 250 КМ",
"RINGS {spacing} NM": {
"one": "КОЛЬЦА {spacing} МОР. МИЛЯ",
"few": "КОЛЬЦА {spacing} МОР. МИЛИ",
"many": "КОЛЬЦА {spacing} МОР. МИЛЬ",
"other": "КОЛЬЦА {spacing} МОР. МИЛИ"
},
"RINGS {spacing} NM": "КОЛЬЦА {spacing} МОР. МИЛ.",
"{count} CONTACTS": {
"one": "{count} КОНТАКТ",
"few": "{count} КОНТАКТА",
@@ -1732,7 +1699,6 @@
"Observed history": "История наблюдений",
"Later observation": "Более позднее наблюдение",
"Latest": "Последние",
"news list|Latest": "Последние",
"Newest frame per product": "Самый свежий кадр каждого продукта",
"Replay observed history": "Воспроизвести историю наблюдений",
"S on another day replaces it · × unpins": "S в другой день заменяет его · × открепляет",
@@ -3588,12 +3554,7 @@
"Photo of {name}": "Фото: {name}",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": {
"one": "{count} наблюдение в кадре",
"few": "{count} наблюдения в кадре",
"many": "{count} наблюдений в кадре",
"other": "{count} наблюдения в кадре"
},
"{count} sightings in view": "Наблюдений в кадре: {count}",
"Close sighting": "Закрыть наблюдение",
"Wildlife observed within 48 h (whales 7 d). Color and badge glyph show the taxon group. Sensitive species are shown only as 0.2° cells; Salish Sea marine mammal reports via Acartia are rounded to 0.01°. Not a census.": "Наблюдения живой природы за 48 ч (китообразные — 7 дней). Цвет и символ значка обозначают таксономическую группу. Уязвимые виды показаны только ячейками 0,2°; сообщения Acartia о морских млекопитающих моря Селиш округлены до 0,01°. Это не учёт численности.",
"Open {name} on iNaturalist": "Открыть {name} в iNaturalist",
@@ -3604,33 +3565,20 @@
"{days}d": "{days} дн.",
"All markers": "Все метки",
"Show markers of every age": "Показать метки любого возраста",
"Maximum event age: {days}d; undated markers are kept": {
"one": "Максимальный возраст события: {days} дн.; метки без даты остаются",
"few": "Максимальный возраст события: {days} дн.; метки без даты остаются",
"many": "Максимальный возраст события: {days} дн.; метки без даты остаются",
"other": "Максимальный возраст события: {days} дн.; метки без даты остаются"
},
"Maximum event age: {days}d; undated markers are kept": "Максимальный возраст события: {days} дн.; метки без даты остаются",
"event age filter|All": "Все",
"Event age": "Возраст события",
"Event age: {days}d · undated kept": {
"one": "Возраст события: {days} дн. · метки без даты остаются",
"few": "Возраст события: {days} дн. · метки без даты остаются",
"many": "Возраст события: {days} дн. · метки без даты остаются",
"other": "Возраст события: {days} дн. · метки без даты остаются"
},
"Event age: {days}d · undated kept": "Возраст события: {days} дн. · метки без даты остаются",
"Close camera preview": "Закрыть просмотр камеры",
"Refresh map still": "Обновить кадр на карте",
"The map still uses a separate request and may show a different moment.": "Кадр на карте запрашивается отдельно и может показывать другой момент.",
"Video plays from Québec 511, which sees your IP address.": "Видео воспроизводится с Québec 511, который видит ваш IP-адрес.",
"Press OPEN to load the photo": "Нажмите ОТКРЫТЬ, чтобы загрузить снимок",
"Captured {month}": "Снято: {month}",
"this spot": "эта точка",
"the view centre": "центр вида",
"{source} at {place}": "{source}: {place}",
"{source} at this spot": "{source} в этой точке",
"{source} at the view centre": "{source} в центре вида",
"Street level at {place}": "Уличные снимки: {place}",
"Street level at this spot": "Уличные снимки в этой точке",
"Street level at the view centre": "Уличные снимки в центре вида",
"Switching map": "Смена карты",
"Opening imagery": "Открытие снимков",
"Street View is ready: press {open} to load it": "Просмотр улиц готов: нажмите {open}, чтобы загрузить",
@@ -3722,7 +3670,6 @@
"{source} imagery could not be opened": "Не удалось открыть снимки {source}",
"{place} has no position to look for imagery at": "У места «{place}» нет координат для поиска снимков",
"No {source} imagery near {place}": "Нет снимков {source} рядом с местом «{place}»",
"No {source} imagery near the view centre": "Нет снимков {source} рядом с центром вида",
"{reason} (filters on: {filters})": "{reason} (включены фильтры: {filters})",
"tiles unavailable": "тайлы недоступны",
"Environment": "Окружающая среда",
@@ -3811,12 +3758,7 @@
"switch on a dataset to load features": "включите набор данных, чтобы загрузить объекты",
"Data Library at a glance": "Библиотека данных вкратце",
"Map features": "Объекты на карте",
"Showing {shown} of {total}": {
"one": "Показано {shown} из {total}",
"few": "Показано {shown} из {total}",
"many": "Показано {shown} из {total}",
"other": "Показано {shown} из {total}"
},
"Showing {shown} of {total}": "Показано {shown} из {total}",
"Datasets": "Наборы данных",
"Themes": "Темы",
"zoom in to load this layer": "приблизьте, чтобы загрузить этот слой",
@@ -3837,5 +3779,12 @@
"500–1,000 m": "500–1000 м",
"1,000–2,000 m": "1000–2000 м",
"2,000–3,000 m": "2000–3000 м",
"3,000 m and above": "3000 м и выше"
"3,000 m and above": "3000 м и выше",
"The globe stopped drawing": "Глобус перестал отрисовываться",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "Браузер отключил 3D-графику глобуса. Слои и элементы управления работают, но карта не отрисуется, пока страница не будет перезагружена.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "Отрисовка остановилась из-за ошибки. Слои и элементы управления работают, но карта не обновляется.",
"Reset view reloads the app without the view restored from the address bar.": "«Сбросить вид» перезагружает приложение без вида, восстановленного из адресной строки.",
"Reload": "Перезагрузить",
"Retry": "Повторить",
"Reset view": "Сбросить вид"
}
+19 -50
View File
@@ -67,30 +67,18 @@
"{severity} and above": "{severity} och uppåt",
"{severity} only": "Endast ”{severity}”",
"All categories": "Alla kategorier",
"Last {hours} h": {
"one": "Senaste {hours} h",
"other": "Senaste {hours} h"
},
"Last {hours} h": "Senaste {hours} h",
"Reading the news…": "Läser in nyheterna …",
"News, last {hours} hours": {
"one": "Nyheter, senaste {hours} h",
"other": "Nyheter, senaste {hours} h"
},
"News, last {hours} hours": "Nyheter, senaste {hours} h",
"Time window": "Tidsfönster",
"News sections": "Nyhetssektioner",
"Hotspots": "Oroshärdar",
"Severity": "Allvarlighetsgrad",
"Category": "Kategori",
"Zoom in to see local news": "Zooma in för att se lokala nyheter",
"Nothing reported near here in the last {hours} h": {
"one": "Inget rapporterat i närheten senaste {hours} h",
"other": "Inget rapporterat i närheten senaste {hours} h"
},
"Nothing reported near here in the last {hours} h": "Inget rapporterat i närheten senaste {hours} h",
"Latest news": "Senaste nyheterna",
"No news matches these filters in the last {hours} h": {
"one": "Inga nyheter matchar filtren senaste {hours} h",
"other": "Inga nyheter matchar filtren senaste {hours} h"
},
"No news matches these filters in the last {hours} h": "Inga nyheter matchar filtren senaste {hours} h",
"Load older news": "Ladda äldre nyheter",
"How to read the map": "Så läses kartan",
"Colour: severity · icon: type of event": "Färg: allvarlighetsgrad · ikon: typ av händelse",
@@ -210,7 +198,6 @@
"Previous — prior visited contact in the 250 km window": "Förra – senast besökta kontakt inom 250 km",
"PREVIOUS": "FÖRRA",
"FOCUS": "FOKUS",
"site status|FOCUS": "FOKUS",
"Next — nearest unvisited contact in the 250 km window": "Nästa – närmaste obesökta kontakt inom 250 km",
"NEXT": "NÄSTA",
"Open-source mapped/observed context. Missing broadcasts, unloaded map areas, or unmapped sites are not evidence of absence.": "Kartlagd/observerad kontext från öppna källor. Uteblivna sändningar, kartområden som inte har laddats eller platser som inte är kartlagda bevisar inte att något saknas.",
@@ -578,7 +565,6 @@
"Reclassify as TR-3B": "Klassa om som TR-3B",
"TRACK ACQUIRED": "SPÅR LÅST",
"{label} · COURSE {heading}°": "{label} · FÄRDRIKTNING {heading}°",
"vessel|{label} · COURSE {heading}°": "{label} · FÄRDRIKTNING {heading}°",
"Context could not restore every layer; try again": "Läget ”Kontext” kunde inte återställa alla lager – försök igen",
"No selected data layers": "Inga valda datalager",
"{count} data layers could not be cleared": {
@@ -2340,7 +2326,6 @@
"Observed history": "Observerad historik",
"Later observation": "Senare observation",
"Latest": "Senaste",
"news list|Latest": "Senaste",
"Newest frame per product": "Senaste bildruta per produkt",
"Replay observed history": "Spela upp observerad historik",
"Imagery on Esri · Google 3D returns when cleared": "Bilder på Esri · Google 3D återkommer när de rensas",
@@ -3064,7 +3049,6 @@
"Left panels": "Vänstra paneler",
"Right panels": "Högra paneler",
"CONFIRMED": "BEKRÄFTAD",
"sightings filter|CONFIRMED": "BEKRÄFTADE",
"PAD / RTLS": "RAMP / RTLS",
"fire confidence filter|All": "Alla",
"fire confidence filter|Nominal+": "Normal+",
@@ -3325,10 +3309,7 @@
"Photo of {name}": "Foto av {name}",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": {
"one": "{count} observation i vyn",
"other": "{count} observationer i vyn"
},
"{count} sightings in view": "Observationer i vyn: {count}",
"Close sighting": "Stäng observationen",
"Wildlife observed within 48 h (whales 7 d). Color and badge glyph show the taxon group. Sensitive species are shown only as 0.2° cells; Salish Sea marine mammal reports via Acartia are rounded to 0.01°. Not a census.": "Djur, växter och svampar observerade de senaste 48 h (valar 7 d). Färg och symbol visar artgruppen. Känsliga arter visas bara som rutor på 0,2°; rapporter om marina däggdjur i Salish Sea via Acartia avrundas till 0,01°. Ingen inventering.",
"Open {name} on iNaturalist": "Öppna {name} på iNaturalist",
@@ -3339,29 +3320,20 @@
"{days}d": "{days} d",
"All markers": "Alla markörer",
"Show markers of every age": "Visa markörer oavsett ålder",
"Maximum event age: {days}d; undated markers are kept": {
"one": "Högsta ålder på händelser: {days} d; markörer utan datum behålls",
"other": "Högsta ålder på händelser: {days} d; markörer utan datum behålls"
},
"Maximum event age: {days}d; undated markers are kept": "Högsta ålder på händelser: {days} d; markörer utan datum behålls",
"event age filter|All": "Alla",
"Event age": "Händelsernas ålder",
"Event age: {days}d · undated kept": {
"one": "Händelsernas ålder: {days} d · utan datum behålls",
"other": "Händelsernas ålder: {days} d · utan datum behålls"
},
"Event age: {days}d · undated kept": "Händelsernas ålder: {days} d · utan datum behålls",
"Close camera preview": "Stäng förhandsvisningen av kameran",
"Refresh map still": "Uppdatera stillbilden på kartan",
"The map still uses a separate request and may show a different moment.": "Stillbilden på kartan hämtas separat och kan visa ett annat ögonblick.",
"Video plays from Québec 511, which sees your IP address.": "Videon spelas upp från Québec 511, som ser din IP-adress.",
"Press OPEN to load the photo": "Tryck på ÖPPNA för att ladda fotot",
"Captured {month}": "Tagen {month}",
"this spot": "den här platsen",
"the view centre": "vyns mitt",
"{source} at {place}": "{source} vid {place}",
"{source} at this spot": "{source} på den här platsen",
"{source} at the view centre": "{source} i vyns mitt",
"Street level at {place}": "Gatunivå vid {place}",
"Street level at this spot": "Gatunivå på den här platsen",
"Street level at the view centre": "Gatunivå i vyns mitt",
"Switching map": "Byter karta",
"Opening imagery": "Öppnar bilder",
"Street View is ready: press {open} to load it": "Street View är redo: tryck på {open} för att ladda den",
@@ -3395,10 +3367,7 @@
"LIVE AIS": "LIVE-AIS",
"CONTACT NAVIGATION OFF": "KONTAKTNAVIGERING AV",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "BRYGGAN STANNAR PÅ DET VALDA FARTYGET",
"{range} NM": {
"one": "{range} NM",
"other": "{range} NM"
},
"{range} NM": "{range} NM",
"AIS DESTINATION · CREW REPORTED": "AIS-DESTINATION · ANGIVEN AV BESÄTTNINGEN",
"{label} · NADIR VIEW": "{label} · NADIRVY",
"SITES STANDBY": "PLATSER I VÄNTLÄGE",
@@ -3477,10 +3446,7 @@
"Zoom in (+)": "Zooma in (+)",
"Zoom to the imaging sensor swath": "Zooma till sensorns avbildningsstråk",
"SENSOR": "SENSOR",
"RINGS {spacing} NM": {
"one": "RINGAR {spacing} NM",
"other": "RINGAR {spacing} NM"
},
"RINGS {spacing} NM": "RINGAR {spacing} NM",
"{count} CONTACTS": {
"one": "{count} KONTAKT",
"other": "{count} KONTAKTER"
@@ -3534,7 +3500,6 @@
"{source} imagery could not be opened": "Bilder från {source} kunde inte öppnas",
"{place} has no position to look for imagery at": "{place} har ingen position att söka efter bilder vid",
"No {source} imagery near {place}": "Inga bilder från {source} nära {place}",
"No {source} imagery near the view centre": "Inga bilder från {source} nära vyns mitt",
"{reason} (filters on: {filters})": "{reason} (aktiva filter: {filters})",
"1 h/s": "1 h/s",
"1 d/s": "1 d/s",
@@ -3639,9 +3604,13 @@
"REMOVE ALL": "TA BORT ALLA",
"Confirm: remove every added entry from Data Layers": "Bekräfta: ta bort alla tillagda poster från Datalager",
"Remove every added entry from Data Layers": "Ta bort alla tillagda poster från Datalager",
"Showing {shown} of {total}": {
"one": "Visar {shown} av {total}",
"other": "Visar {shown} av {total}"
},
"BROWSE DATA LIBRARY": "BLÄDDRA I DATABIBLIOTEKET"
"Showing {shown} of {total}": "Visar {shown} av {total}",
"BROWSE DATA LIBRARY": "BLÄDDRA I DATABIBLIOTEKET",
"The globe stopped drawing": "Globen ritas inte längre",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "Webbläsaren tog tillbaka globens 3D-grafik. Lager och reglage fungerar fortfarande, men kartan kan inte ritas förrän sidan laddas om.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "Renderingen stoppades efter ett fel. Lager och reglage fungerar fortfarande, men kartan uppdateras inte.",
"Reset view reloads the app without the view restored from the address bar.": "”Återställ vy” laddar om appen utan vyn som återställdes från adressfältet.",
"Reload": "Ladda om",
"Retry": "Försök igen",
"Reset view": "Återställ vy"
}
+20 -71
View File
@@ -71,18 +71,8 @@
"{severity} only": "Лише {severity}",
"All categories": "Усі категорії",
"Reading the news…": "Читаємо новини…",
"News, last {hours} hours": {
"one": "Новини за останню {hours} годину",
"few": "Новини за останні {hours} години",
"many": "Новини за останні {hours} годин",
"other": "Новини за останні {hours} години"
},
"Last {hours} h": {
"one": "За {hours} год",
"few": "За {hours} год",
"many": "За {hours} год",
"other": "За {hours} год"
},
"News, last {hours} hours": "Новини за {hours} год",
"Last {hours} h": "За {hours} год",
"Time window": "Період",
"Conflicts": "Конфлікти",
"Local": "Місцеві",
@@ -91,19 +81,9 @@
"Hotspots": "Гарячі точки",
"Severity": "Тяжкість",
"Category": "Категорія",
"Nothing reported near here in the last {hours} h": {
"one": "За останню {hours} год поблизу нічого не повідомлялося",
"few": "За останні {hours} год поблизу нічого не повідомлялося",
"many": "За останні {hours} год поблизу нічого не повідомлялося",
"other": "За останні {hours} год поблизу нічого не повідомлялося"
},
"Nothing reported near here in the last {hours} h": "За {hours} год поблизу нічого не повідомлялося",
"Latest news": "Останні новини",
"No news matches these filters in the last {hours} h": {
"one": "За останню {hours} год немає новин за цими фільтрами",
"few": "За останні {hours} год немає новин за цими фільтрами",
"many": "За останні {hours} год немає новин за цими фільтрами",
"other": "За останні {hours} год немає новин за цими фільтрами"
},
"No news matches these filters in the last {hours} h": "За {hours} год немає новин за цими фільтрами",
"Load older news": "Завантажити давніші новини",
"How to read the map": "Як читати карту",
"Colour: severity · icon: type of event": "Колір: тяжкість · значок: тип події",
@@ -614,7 +594,6 @@
"Previous — prior visited contact in the 250 km window": "Назад — попередній переглянутий контакт у радіусі 250 км",
"PREVIOUS": "НАЗАД",
"FOCUS": "ФОКУС",
"site status|FOCUS": "ФОКУС",
"Next — nearest unvisited contact in the 250 km window": "Далі — найближчий ще не переглянутий контакт у радіусі 250 км",
"NEXT": "ДАЛІ",
"Named areas ({n})": "Іменовані зони ({n})",
@@ -977,7 +956,7 @@
"ORBITAL VELOCITY": "ОРБІТАЛЬНА ШВИДКІСТЬ",
"SUB-SATELLITE POINT": "ПІДСУПУТНИКОВА ТОЧКА",
"NORTH UP": "ПІВНІЧ УГОРІ",
"CURRENTLY OVER": "ПІД СУПУТНИКОМ",
"CURRENTLY OVER": "ЗАРАЗ НАД",
"{division}, {country}": "{division}, {country}",
"{region} · near {place}": "{region} · поблизу: {place}",
"NORAD {id}": "NORAD {id}",
@@ -1880,12 +1859,7 @@
"ROUTE DATA UNAVAILABLE": "ДАНІ МАРШРУТУ НЕДОСТУПНІ",
"CONTACT NAVIGATION OFF": "НАВІГАЦІЮ КОНТАКТАМИ ВИМКНЕНО",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "МІСТОК ЗАЛИШАЄТЬСЯ НА ВИБРАНОМУ СУДНІ",
"{range} NM": {
"one": "{range} МОР. МИЛЯ",
"few": "{range} МОР. МИЛІ",
"many": "{range} МОР. МИЛЬ",
"other": "{range} МОР. МИЛІ"
},
"{range} NM": "{range} МОР. МИЛ.",
"AIS DESTINATION · CREW REPORTED": "ПРИЗНАЧЕННЯ AIS · ВКАЗАНО ЕКІПАЖЕМ",
"DEST {bearing}": "ПРИЗН. {bearing}",
"Disable cockpit weather effects": "Вимкнути погодні ефекти кабіни",
@@ -1934,7 +1908,6 @@
"Reclassify as TR-3B": "Перекласифікувати як TR-3B",
"TRACK ACQUIRED": "ТРЕК ЗАХОПЛЕНО",
"{label} · COURSE {heading}°": "{label} · ШЛЯХОВИЙ КУТ {heading}°",
"vessel|{label} · COURSE {heading}°": "{label} · ШЛЯХОВИЙ КУТ {heading}°",
"{label} · HEADING {heading}°": "{label} · КУРС {heading}°",
"{label} · NO HEADING": "{label} · НЕМАЄ КУРСУ",
"{label} · NADIR VIEW": "{label} · ВИГЛЯД У НАДИР",
@@ -2215,7 +2188,6 @@
"Observed history": "Історія спостережень",
"Later observation": "Наступне спостереження",
"Latest": "Останні",
"news list|Latest": "Останні",
"Newest frame per product": "Найсвіжіший кадр кожного продукту",
"Replay observed history": "Відтворити історію спостережень",
"Imagery on Esri · Google 3D returns when cleared": "Підкладка: Esri · скидання поверне Google 3D",
@@ -2982,12 +2954,7 @@
"Contact cockpit summary": "Зведення контакту в кабіні",
"CONTACT": "КОНТАКТ",
"CONTACTS · 250 KM": "КОНТАКТИ · 250 КМ",
"RINGS {spacing} NM": {
"one": "КІЛЬЦЯ {spacing} МОР. МИЛЯ",
"few": "КІЛЬЦЯ {spacing} МОР. МИЛІ",
"many": "КІЛЬЦЯ {spacing} МОР. МИЛЬ",
"other": "КІЛЬЦЯ {spacing} МОР. МИЛІ"
},
"RINGS {spacing} NM": "КІЛЬЦЯ {spacing} МОР. МИЛ.",
"{count} CONTACTS": {
"one": "{count} КОНТАКТ",
"few": "{count} КОНТАКТИ",
@@ -3328,7 +3295,6 @@
"Left panels": "Ліві панелі",
"Right panels": "Праві панелі",
"CONFIRMED": "ПІДТВЕРДЖЕНО",
"sightings filter|CONFIRMED": "ПІДТВЕРДЖЕНО",
"PAD / RTLS": "МІСЦЕ ЗАПУСКУ / RTLS",
"Name of {name}": "Назва: {name}",
"Change the colour of {name}": "Змінити колір: {name}",
@@ -3588,12 +3554,7 @@
"Photo of {name}": "Фото {name}",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": {
"one": "{count} спостереження в кадрі",
"few": "{count} спостереження в кадрі",
"many": "{count} спостережень у кадрі",
"other": "{count} спостереження в кадрі"
},
"{count} sightings in view": "Спостережень у кадрі: {count}",
"Close sighting": "Закрити спостереження",
"Wildlife observed within 48 h (whales 7 d). Color and badge glyph show the taxon group. Sensitive species are shown only as 0.2° cells; Salish Sea marine mammal reports via Acartia are rounded to 0.01°. Not a census.": "Спостереження живої природи за 48 год (китоподібні — 7 днів). Колір і символ значка позначають таксономічну групу. Вразливі види показано лише комірками 0,2°; повідомлення Acartia про морських ссавців моря Селіш округлено до 0,01°. Не перепис.",
"Open {name} on iNaturalist": "Відкрити {name} в iNaturalist",
@@ -3604,33 +3565,20 @@
"{days}d": "{days} дн",
"All markers": "Усі мітки",
"Show markers of every age": "Показати мітки будь-якого віку",
"Maximum event age: {days}d; undated markers are kept": {
"one": "Максимальний вік події: {days} дн; мітки без дати залишаються",
"few": "Максимальний вік події: {days} дн; мітки без дати залишаються",
"many": "Максимальний вік події: {days} дн; мітки без дати залишаються",
"other": "Максимальний вік події: {days} дн; мітки без дати залишаються"
},
"Maximum event age: {days}d; undated markers are kept": "Максимальний вік події: {days} дн; мітки без дати залишаються",
"event age filter|All": "Усі",
"Event age": "Вік події",
"Event age: {days}d · undated kept": {
"one": "Вік події: {days} дн · мітки без дати залишаються",
"few": "Вік події: {days} дн · мітки без дати залишаються",
"many": "Вік події: {days} дн · мітки без дати залишаються",
"other": "Вік події: {days} дн · мітки без дати залишаються"
},
"Event age: {days}d · undated kept": "Вік події: {days} дн · мітки без дати залишаються",
"Close camera preview": "Закрити перегляд камери",
"Refresh map still": "Оновити кадр на карті",
"The map still uses a separate request and may show a different moment.": "Кадр на карті запитується окремо та може показувати інший момент.",
"Video plays from Québec 511, which sees your IP address.": "Відео відтворюється з Québec 511, який бачить вашу IP-адресу.",
"Press OPEN to load the photo": "Натисніть «ВІДКРИТИ», щоб завантажити фото",
"Captured {month}": "Знято: {month}",
"this spot": "це місце",
"the view centre": "центр карти",
"{source} at {place}": "{source}: {place}",
"{source} at this spot": "{source} у цьому місці",
"{source} at the view centre": "{source} у центрі карти",
"Street level at {place}": "Вуличні знімки: {place}",
"Street level at this spot": "Вуличні знімки в цьому місці",
"Street level at the view centre": "Вуличні знімки в центрі карти",
"Switching map": "Перемикання карти",
"Opening imagery": "Відкриття знімків",
"Street View is ready: press {open} to load it": "Перегляд вулиць готовий: натисніть «{open}», щоб завантажити",
@@ -3722,7 +3670,6 @@
"{source} imagery could not be opened": "Не вдалося відкрити знімки {source}",
"{place} has no position to look for imagery at": "Для місця «{place}» немає координат для пошуку знімків",
"No {source} imagery near {place}": "Немає знімків {source} поблизу: {place}",
"No {source} imagery near the view centre": "Немає знімків {source} поблизу центру карти",
"{reason} (filters on: {filters})": "{reason} (увімкнено фільтри: {filters})",
"tiles unavailable": "тайли недоступні",
"zoom in to load this layer": "наблизьте карту, щоб завантажити цей шар",
@@ -3831,11 +3778,13 @@
"REMOVE ALL": "ВИДАЛИТИ ВСЕ",
"Confirm: remove every added entry from Data Layers": "Підтвердити: видалити всі додані записи з «Шарів даних»",
"Remove every added entry from Data Layers": "Видалити всі додані записи з «Шарів даних»",
"Showing {shown} of {total}": {
"one": "Показано {shown} з {total}",
"few": "Показано {shown} з {total}",
"many": "Показано {shown} з {total}",
"other": "Показано {shown} з {total}"
},
"BROWSE DATA LIBRARY": "ВІДКРИТИ БІБЛІОТЕКУ ДАНИХ"
"Showing {shown} of {total}": "Показано {shown} з {total}",
"BROWSE DATA LIBRARY": "ВІДКРИТИ БІБЛІОТЕКУ ДАНИХ",
"The globe stopped drawing": "Глобус перестав відмальовуватися",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "Браузер вимкнув 3D-графіку глобуса. Шари та елементи керування працюють, але карта не відмалюється, доки сторінку не буде перезавантажено.",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "Відмальовування зупинилося через помилку. Шари та елементи керування працюють, але карта не оновлюється.",
"Reset view reloads the app without the view restored from the address bar.": "«Скинути вигляд» перезавантажує застосунок без вигляду, відновленого з адресного рядка.",
"Reload": "Перезавантажити",
"Retry": "Повторити",
"Reset view": "Скинути вигляд"
}
+19 -40
View File
@@ -62,12 +62,8 @@
"{severity} only": "仅{severity}",
"All categories": "所有类别",
"Reading the news…": "正在读取新闻…",
"News, last {hours} hours": {
"other": "最近 {hours} 小时的新闻"
},
"Last {hours} h": {
"other": "最近 {hours} 小时"
},
"News, last {hours} hours": "最近 {hours} 小时的新闻",
"Last {hours} h": "最近 {hours} 小时",
"Time window": "时间范围",
"Conflicts": "冲突",
"Local": "本地",
@@ -76,13 +72,9 @@
"Hotspots": "热点",
"Severity": "严重程度",
"Category": "类别",
"Nothing reported near here in the last {hours} h": {
"other": "最近 {hours} 小时附近无报道"
},
"Nothing reported near here in the last {hours} h": "最近 {hours} 小时附近无报道",
"Latest news": "最新新闻",
"No news matches these filters in the last {hours} h": {
"other": "最近 {hours} 小时没有符合这些筛选条件的新闻"
},
"No news matches these filters in the last {hours} h": "最近 {hours} 小时没有符合这些筛选条件的新闻",
"Load older news": "加载更早的新闻",
"How to read the map": "如何解读地图",
"Colour: severity · icon: type of event": "颜色:严重程度 · 图标:事件类型",
@@ -571,7 +563,6 @@
"Previous — prior visited contact in the 250 km window": "上一个:250 km 范围内先前访问过的目标",
"PREVIOUS": "上一个",
"FOCUS": "聚焦",
"site status|FOCUS": "聚焦",
"Next — nearest unvisited contact in the 250 km window": "下一个:250 km 范围内最近的未访问目标",
"NEXT": "下一个",
"Named areas ({n})": "已命名区域({n})",
@@ -938,7 +929,6 @@
"Cancel replay": "取消回放",
"EST. DOWNRANGE": "预估落点距离",
"CONFIRMED": "已确认",
"sightings filter|CONFIRMED": "已确认",
"PAD / RTLS": "发射台 / RTLS",
"PLANNED · ": "计划 · ",
"SUPPLIED TRAJECTORY POINTS": "提供的轨迹点",
@@ -1760,9 +1750,7 @@
"ROUTE DATA UNAVAILABLE": "航线数据不可用",
"CONTACT NAVIGATION OFF": "目标导航已关闭",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "驾驶台保持在所选船舶上",
"{range} NM": {
"other": "{range} 海里"
},
"{range} NM": "{range} 海里",
"AIS DESTINATION · CREW REPORTED": "AIS 目的地 · 船员申报",
"DEST {bearing}": "目的地 {bearing}",
"Disable cockpit weather effects": "停用座舱天气效果",
@@ -1808,7 +1796,6 @@
"Reclassify as TR-3B": "重新归类为 TR-3B",
"TRACK ACQUIRED": "已获取航迹",
"{label} · COURSE {heading}°": "{label} · 对地航向 {heading}°",
"vessel|{label} · COURSE {heading}°": "{label} · 对地航向 {heading}°",
"{label} · HEADING {heading}°": "{label} · 船首向 {heading}°",
"{label} · NO HEADING": "{label} · 无船首向",
"{label} · NADIR VIEW": "{label} · 天底视图",
@@ -2061,7 +2048,6 @@
"Observed history": "观测历史",
"Later observation": "较晚观测",
"Latest": "最新",
"news list|Latest": "最新",
"Newest frame per product": "每个产品的最新一帧",
"Replay observed history": "回放观测历史",
"Imagery on Esri · Google 3D returns when cleared": "影像显示在 Esri 底图上 · 清除后恢复 Google 3D",
@@ -2643,9 +2629,7 @@
"Contact cockpit summary": "目标座舱摘要",
"CONTACT": "目标",
"CONTACTS · 250 KM": "目标 · 250 km",
"RINGS {spacing} NM": {
"other": "距标圈 {spacing} 海里"
},
"RINGS {spacing} NM": "距标圈 {spacing} 海里",
"{count} CONTACTS": {
"other": "{count} 个目标"
},
@@ -3327,9 +3311,7 @@
"Photo of {name}": "{name} 的照片",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": {
"other": "视野内有 {count} 次目击"
},
"{count} sightings in view": "视野内有 {count} 次目击",
"Close sighting": "关闭目击记录",
"Wildlife observed within 48 h (whales 7 d). Color and badge glyph show the taxon group. Sensitive species are shown only as 0.2° cells; Salish Sea marine mammal reports via Acartia are rounded to 0.01°. Not a census.": "近 48 小时内观察到的野生生物(鲸类为 7 天)。颜色与徽章图标表示分类群。敏感物种仅以 0.2° 网格显示;Acartia 提供的萨利希海海洋哺乳动物目击位置四舍五入到 0.01°。并非种群普查。",
"Open {name} on iNaturalist": "在 iNaturalist 上打开 {name}",
@@ -3340,27 +3322,20 @@
"{days}d": "{days} 天",
"All markers": "所有标记",
"Show markers of every age": "显示所有时间的标记",
"Maximum event age: {days}d; undated markers are kept": {
"other": "仅显示 {days} 天内的事件;保留无日期标记"
},
"Maximum event age: {days}d; undated markers are kept": "仅显示 {days} 天内的事件;保留无日期标记",
"event age filter|All": "全部",
"Event age": "事件时间",
"Event age: {days}d · undated kept": {
"other": "事件时间:{days} 天内 · 保留无日期标记"
},
"Event age: {days}d · undated kept": "事件时间:{days} 天内 · 保留无日期标记",
"Close camera preview": "关闭摄像头预览",
"Refresh map still": "刷新地图静止画面",
"The map still uses a separate request and may show a different moment.": "地图静止画面通过单独的请求获取,可能显示不同的时刻。",
"Video plays from Québec 511, which sees your IP address.": "视频从 Québec 511 播放,Québec 511 会看到你的 IP 地址。",
"Press OPEN to load the photo": "点击“打开”加载照片",
"Captured {month}": "拍摄于 {month}",
"this spot": "此处",
"the view centre": "视野中心",
"{source} at {place}": "{place} 的 {source}",
"{source} at this spot": "此处的 {source}",
"{source} at the view centre": "视图中心的 {source}",
"Street level at {place}": "{place} 的街景",
"Street level at this spot": "此处的街景",
"Street level at the view centre": "视图中心的街景",
"Switching map": "正在切换地图",
"Opening imagery": "正在打开影像",
"Street View is ready: press {open} to load it": "街景已就绪:点击“{open}”加载",
@@ -3452,7 +3427,6 @@
"{source} imagery could not be opened": "无法打开 {source} 影像",
"{place} has no position to look for imagery at": "{place} 没有可查找影像的位置",
"No {source} imagery near {place}": "{place} 附近没有 {source} 影像",
"No {source} imagery near the view centre": "视图中心附近没有 {source} 影像",
"{reason} (filters on: {filters})": "{reason}(已开启筛选:{filters})",
"tiles unavailable": "瓦片不可用",
"zoom in to load this layer": "放大以加载此图层",
@@ -3543,8 +3517,13 @@
"REMOVE ALL": "全部移除",
"Confirm: remove every added entry from Data Layers": "确认:从数据图层中移除所有已添加条目",
"Remove every added entry from Data Layers": "从数据图层中移除所有已添加条目",
"Showing {shown} of {total}": {
"other": "显示 {shown} / {total}"
},
"BROWSE DATA LIBRARY": "浏览数据资源库"
"Showing {shown} of {total}": "显示 {shown} / {total}",
"BROWSE DATA LIBRARY": "浏览数据资源库",
"The globe stopped drawing": "地球已停止绘制",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "浏览器收回了地球的 3D 图形。图层和控件仍可使用,但在重新加载页面之前地图无法绘制。",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "渲染因错误而停止。图层和控件仍可使用,但地图不再更新。",
"Reset view reloads the app without the view restored from the address bar.": "“重置视图”会重新加载应用,不使用从地址栏恢复的视图。",
"Reload": "重新加载",
"Retry": "重试",
"Reset view": "重置视图"
}
+19 -40
View File
@@ -62,12 +62,8 @@
"{severity} only": "僅{severity}",
"All categories": "所有類別",
"Reading the news…": "正在讀取新聞…",
"News, last {hours} hours": {
"other": "最近 {hours} 小時的新聞"
},
"Last {hours} h": {
"other": "最近 {hours} 小時"
},
"News, last {hours} hours": "最近 {hours} 小時的新聞",
"Last {hours} h": "最近 {hours} 小時",
"Time window": "時間範圍",
"Conflicts": "衝突",
"Local": "本地",
@@ -76,13 +72,9 @@
"Hotspots": "熱點",
"Severity": "嚴重程度",
"Category": "類別",
"Nothing reported near here in the last {hours} h": {
"other": "最近 {hours} 小時附近無報道"
},
"Nothing reported near here in the last {hours} h": "最近 {hours} 小時附近無報道",
"Latest news": "最新新聞",
"No news matches these filters in the last {hours} h": {
"other": "最近 {hours} 小時沒有符合這些篩選條件的新聞"
},
"No news matches these filters in the last {hours} h": "最近 {hours} 小時沒有符合這些篩選條件的新聞",
"Load older news": "加載更早的新聞",
"How to read the map": "如何解讀地圖",
"Colour: severity · icon: type of event": "顏色:嚴重程度 · 圖標:事件類型",
@@ -571,7 +563,6 @@
"Previous — prior visited contact in the 250 km window": "上一個:250 km 範圍內先前訪問過的目標",
"PREVIOUS": "上一個",
"FOCUS": "聚焦",
"site status|FOCUS": "聚焦",
"Next — nearest unvisited contact in the 250 km window": "下一個:250 km 範圍內最近的未訪問目標",
"NEXT": "下一個",
"Named areas ({n})": "已命名區域({n})",
@@ -933,7 +924,6 @@
"Cancel replay": "取消回放",
"EST. DOWNRANGE": "預估落點距離",
"CONFIRMED": "已確認",
"sightings filter|CONFIRMED": "已確認",
"PAD / RTLS": "發射臺 / RTLS",
"PLANNED · ": "計劃 · ",
"SUPPLIED TRAJECTORY POINTS": "提供的軌跡點",
@@ -1752,9 +1742,7 @@
"ROUTE DATA UNAVAILABLE": "航線數據不可用",
"CONTACT NAVIGATION OFF": "目標導航已關閉",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "駕駛臺保持在所選船舶上",
"{range} NM": {
"other": "{range} 海里"
},
"{range} NM": "{range} 海里",
"AIS DESTINATION · CREW REPORTED": "AIS 目的地 · 船員申報",
"DEST {bearing}": "目的地 {bearing}",
"Disable cockpit weather effects": "停用座艙天氣效果",
@@ -1800,7 +1788,6 @@
"Reclassify as TR-3B": "重新歸類為 TR-3B",
"TRACK ACQUIRED": "已獲取航跡",
"{label} · COURSE {heading}°": "{label} · 對地航向 {heading}°",
"vessel|{label} · COURSE {heading}°": "{label} · 對地航向 {heading}°",
"{label} · HEADING {heading}°": "{label} · 船首向 {heading}°",
"{label} · NO HEADING": "{label} · 無船首向",
"{label} · NADIR VIEW": "{label} · 天底視圖",
@@ -2051,7 +2038,6 @@
"Observed history": "觀測歷史",
"Later observation": "較晚觀測",
"Latest": "最新",
"news list|Latest": "最新",
"Newest frame per product": "每個產品的最新一幀",
"Replay observed history": "回放觀測歷史",
"Imagery on Esri · Google 3D returns when cleared": "影像顯示在 Esri 底圖上 · 清除後恢復 Google 3D",
@@ -2633,9 +2619,7 @@
"Contact cockpit summary": "目標座艙摘要",
"CONTACT": "目標",
"CONTACTS · 250 KM": "目標 · 250 km",
"RINGS {spacing} NM": {
"other": "距標圈 {spacing} 海里"
},
"RINGS {spacing} NM": "距標圈 {spacing} 海里",
"{count} CONTACTS": {
"other": "{count} 個目標"
},
@@ -3327,9 +3311,7 @@
"Photo of {name}": "{name} 的照片",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": {
"other": "視野內有 {count} 次目擊"
},
"{count} sightings in view": "視野內有 {count} 次目擊",
"Close sighting": "關閉目擊記錄",
"Wildlife observed within 48 h (whales 7 d). Color and badge glyph show the taxon group. Sensitive species are shown only as 0.2° cells; Salish Sea marine mammal reports via Acartia are rounded to 0.01°. Not a census.": "近 48 小時內觀察到的野生生物(鯨類為 7 天)。顏色與徽章圖標表示分類群。敏感物種僅以 0.2° 網格顯示;Acartia 提供的薩利希海海洋哺乳動物目擊位置四捨五入到 0.01°。並非種群普查。",
"Open {name} on iNaturalist": "在 iNaturalist 上打開 {name}",
@@ -3340,27 +3322,20 @@
"{days}d": "{days} 天",
"All markers": "所有標記",
"Show markers of every age": "顯示所有時間的標記",
"Maximum event age: {days}d; undated markers are kept": {
"other": "僅顯示 {days} 天內的事件;保留無日期標記"
},
"Maximum event age: {days}d; undated markers are kept": "僅顯示 {days} 天內的事件;保留無日期標記",
"event age filter|All": "全部",
"Event age": "事件時間",
"Event age: {days}d · undated kept": {
"other": "事件時間:{days} 天內 · 保留無日期標記"
},
"Event age: {days}d · undated kept": "事件時間:{days} 天內 · 保留無日期標記",
"Close camera preview": "關閉攝像頭預覽",
"Refresh map still": "刷新地圖靜止畫面",
"The map still uses a separate request and may show a different moment.": "地圖靜止畫面通過單獨的請求獲取,可能顯示不同的時刻。",
"Video plays from Québec 511, which sees your IP address.": "視頻從 Québec 511 播放,Québec 511 會看到你的 IP 地址。",
"Press OPEN to load the photo": "點擊“打開”加載照片",
"Captured {month}": "拍攝於 {month}",
"this spot": "此處",
"the view centre": "視野中心",
"{source} at {place}": "{place} 的 {source}",
"{source} at this spot": "此處的 {source}",
"{source} at the view centre": "視圖中心的 {source}",
"Street level at {place}": "{place} 的街景",
"Street level at this spot": "此處的街景",
"Street level at the view centre": "視圖中心的街景",
"Switching map": "正在切換地圖",
"Opening imagery": "正在打開影像",
"Street View is ready: press {open} to load it": "街景已就緒:點擊“{open}”加載",
@@ -3452,7 +3427,6 @@
"{source} imagery could not be opened": "無法打開 {source} 影像",
"{place} has no position to look for imagery at": "{place} 沒有可查找影像的位置",
"No {source} imagery near {place}": "{place} 附近沒有 {source} 影像",
"No {source} imagery near the view centre": "視圖中心附近沒有 {source} 影像",
"{reason} (filters on: {filters})": "{reason}(已開啟篩選:{filters})",
"tiles unavailable": "瓦片不可用",
"zoom in to load this layer": "放大以加載此圖層",
@@ -3543,8 +3517,13 @@
"REMOVE ALL": "全部移除",
"Confirm: remove every added entry from Data Layers": "確認:從數據圖層中移除所有已添加條目",
"Remove every added entry from Data Layers": "從數據圖層中移除所有已添加條目",
"Showing {shown} of {total}": {
"other": "顯示 {shown} / {total}"
},
"BROWSE DATA LIBRARY": "瀏覽數據資源庫"
"Showing {shown} of {total}": "顯示 {shown} / {total}",
"BROWSE DATA LIBRARY": "瀏覽數據資源庫",
"The globe stopped drawing": "地球已停止繪製",
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "瀏覽器收回了地球的 3D 圖形。圖層和控件仍可使用,但在重新加載頁面之前地圖無法繪製。",
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "渲染因錯誤而停止。圖層和控件仍可使用,但地圖不再更新。",
"Reset view reloads the app without the view restored from the address bar.": "「重置視圖」會重新加載應用,不使用從地址欄恢復的視圖。",
"Reload": "重新加載",
"Retry": "重試",
"Reset view": "重置視圖"
}
-1
View File
@@ -685,7 +685,6 @@
"FLIR",
"FM MHZ",
"FOCUS",
"site status|FOCUS",
"FOV",
"FOV {value}°",
"FT",
+4 -7
View File
@@ -762,12 +762,9 @@ export function createGhostMapsLayer({
title:
days === null
? t('Show markers of every age')
: tn(
days,
'Maximum event age: {days}d; undated markers are kept',
'Maximum event age: {days}d; undated markers are kept',
{ days: formatNumber(days) },
),
: t('Maximum event age: {days}d; undated markers are kept', {
days: formatNumber(days),
}),
active: days === _ageDays,
onClick: () => setAgeDays(days),
})),
@@ -802,7 +799,7 @@ export function createGhostMapsLayer({
};
}),
legend: [],
info: `${_ageDays === null ? t('All markers') : tn(_ageDays, 'Event age: {days}d · undated kept', 'Event age: {days}d · undated kept', { days: formatNumber(_ageDays) })} · ${info}`,
info: `${_ageDays === null ? t('All markers') : t('Event age: {days}d · undated kept', { days: formatNumber(_ageDays) })} · ${info}`,
infoTitle: t(
'S2 Underground publishes these KMZ products to GitHub. New uploads are detected within seconds and replace the previous edition automatically. Click a marker for its report.',
),
-30
View File
@@ -1,7 +1,6 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import * as Cesium from 'cesium';
import { setLocale } from '../../i18n/index.js';
import { createGhostMapsLayer } from './index.js';
import { GHOSTMAPS_PRODUCTS, GHOSTMAPS_LATEST_DROP } from './catalog.js';
@@ -249,35 +248,6 @@ test('age controls default to seven days and widening restores filtered and expi
assert.equal(ageChip(layer, null).active, true);
});
test('the event age agrees with its number of days', async (t) => {
t.after(() => setLocale('en'));
const { layer, viewer } = fixture(t, { reports: [{ name: 'Undated' }] });
await layer.update(viewer);
const info = () =>
layer.getRowControls().info.split(' · ').slice(0, 2).join(' · ');
assert.equal(
ageChip(layer, 1).title,
'Maximum event age: 1d; undated markers are kept',
);
setLocale('de');
assert.equal(
ageChip(layer, 1).title,
'Maximales Ereignisalter: 1 Tag; undatierte Markierungen bleiben erhalten',
);
assert.equal(
ageChip(layer, 7).title,
'Maximales Ereignisalter: 7 Tage; undatierte Markierungen bleiben erhalten',
);
assert.equal(info(), 'Ereignisalter: 7 Tage · undatierte bleiben');
await ageChip(layer, 1).onClick();
assert.equal(info(), 'Ereignisalter: 1 Tag · undatierte bleiben');
setLocale('es');
assert.equal(
info(),
'Antigüedad: 1 día · se conservan los marcadores sin fecha',
);
});
test('age preferences persist independently of product choices and reject invalid storage', async (t) => {
const values = new Map([['gev.ghostmaps.products', '["cip"]']]);
const storage = {
@@ -69,38 +69,3 @@ test('the mission panel translates only the fallback status', (t) => {
assert.equal(shownStatus(UNKNOWN_LAUNCH_STATUS), 'Desconocido');
assert.equal(shownStatus('Launch Successful'), 'Launch Successful');
});
test('a confirmed landing position is worded apart from the CONFIRMED sightings chip', (t) => {
t.after(() => setLocale('en'));
const position = () =>
createPanel({
state: {
_selectedLaunchId: 'mission-1',
_launches: [
{
id: 'mission-1',
name: 'Example',
status: 'Launch Successful',
payloads: [],
recoveryStages: [
{
name: 'B1080',
endpoint: { lat: 28.41, lon: -80.6, accuracy: 'CONFIRMED' },
},
],
},
],
_replayTracks: new Map(),
},
parts: {
overlays: { shortMissionLabel: (name) => name },
policyHelpers: { missionPathPresentation: () => ({}) },
replay: { presentReplay: () => ({}) },
},
}).presentMission().stages[0].position;
assert.match(position(), / · CONFIRMED$/);
// One landing, singular: sv BEKRÄFTAD, where the sightings chip says
// BEKRÄFTADE.
setLocale('sv');
assert.match(position(), / · BEKRÄFTAD$/);
});
@@ -542,7 +542,7 @@ test('retention never revives an incumbent card after the observation window exp
test('all heights and the sampled fallback use the same sightings-in-view count label', () => {
const h = harness();
h.state.renderedRows = [sighting];
assert.equal(h.layer.getStats().countLabel, '1 sighting in view');
assert.equal(h.layer.getStats().countLabel, '1 sightings in view');
h.state.sightingCount = 2000;
assert.equal(h.layer.getStats().count, 2000);
assert.equal(
@@ -560,32 +560,6 @@ test('all heights and the sampled fallback use the same sightings-in-view count
}
});
test('the sightings count takes the plural form of its number', (t) => {
t.after(() => setLocale('en'));
const h = harness();
const label = (count) => {
h.state.sightingCount = count;
return h.layer.getStats().countLabel;
};
setLocale('ru');
assert.equal(label(1), '1 наблюдение в кадре');
assert.equal(label(3), '3 наблюдения в кадре');
assert.equal(label(5), '5 наблюдений в кадре');
setLocale('de');
assert.equal(label(1), '1 Beobachtung im Bild');
assert.equal(label(2000), '2.000 Beobachtungen im Bild');
});
test('the CONFIRMED chip names the sightings, apart from a confirmed launch landing', (t) => {
t.after(() => setLocale('en'));
const h = harness();
assert.equal(h.chip('quality-confirmed').label, 'CONFIRMED');
setLocale('sv');
assert.equal(h.chip('quality-confirmed').label, 'BEKRÄFTADE');
setLocale('es');
assert.equal(h.chip('quality-confirmed').label, 'CONFIRMADAS');
});
test('a stale fallback with a failed-refresh deadline remains DEGRADED, retryable and dated to the cached data', async () => {
const h = harness({
getSnapshotDetail: async () => ({
+5 -9
View File
@@ -1,4 +1,4 @@
import { t, tn, formatNumber, translateKnown } from '../../i18n/index.js';
import { t, formatNumber, translateKnown } from '../../i18n/index.js';
import { msg } from '../../i18n/marker.js';
import { translateFeedError } from '../../ui/statusText.js';
import { wildlifeIconicGroup } from './model.js';
@@ -137,8 +137,7 @@ export function createControls({ state, services, parts }) {
const chips = [
chip({
id: WILDLIFE_CONFIRMED_CHIP_ID,
// The sightings, plural; a launch landing is CONFIRMED alone.
label: t('CONFIRMED', { context: 'sightings filter' }),
label: t('CONFIRMED'),
title: t('Show only community-confirmed (research-grade) sightings'),
params: {
confirmed: confirmedFailed ? state.confirmed : !state.confirmed,
@@ -215,12 +214,9 @@ export function createControls({ state, services, parts }) {
stale: state.stale,
loading,
refreshing,
countLabel: tn(
count,
'{count} sighting in view',
'{count} sightings in view',
{ count: formatNumber(count) },
),
countLabel: t('{count} sightings in view', {
count: formatNumber(count),
}),
loadingLabel: loading
? t('loading...')
: refreshing
@@ -1987,7 +1987,7 @@ test('unavailable grids fail closed to visible sampled sprites without a global
assert.equal(h.clicks.collection.values[0].id, 'wildlife:inat:1');
assert.ok(h.clicks.collection.values[0].width * 0.5 >= 8);
assert.equal(h.layer.getStats().count, 1);
assert.equal(h.layer.getStats().countLabel, '1 sighting in view');
assert.match(h.layer.getStats().countLabel, /sightings in view/);
assert.equal(h.layer.getStats().partial, undefined);
assert.equal(layerFeedState(h.layer.getStats()), 'degraded');
assert.match(h.layer.getStats().error, /Malformed iNaturalist grid response/);
@@ -2024,7 +2024,7 @@ test('an oversized view coarsens to a complete grid within the six-tile cap', as
await h.update();
assert.equal(h.clicks.collection.values.length, 1);
assert.equal(h.clicks.collection.values[0].id, 'wildlife:inat:1');
assert.match(h.layer.getStats().countLabel, /sightings? in view/);
assert.match(h.layer.getStats().countLabel, /sightings in view/);
assert.equal(layerFeedState(h.layer.getStats()), 'nominal');
h.layer.destroy();
});
-40
View File
@@ -7,7 +7,6 @@ import assert from 'node:assert/strict';
import { createPinia } from 'pinia';
import { createApp, defineComponent, h, nextTick } from 'vue';
import { CockpitViewController } from './cockpitController.js';
import { setLocale } from '../i18n/index.js';
import { useCockpitStore } from './stores/cockpit.js';
import CockpitView from './views/CockpitView.vue';
import ContextView from './views/ContextView.vue';
@@ -356,33 +355,6 @@ test('the bridge radar shows nearby ships without its own and steps its range',
assert.ok(!$('#cockpit-radar') || $('#cockpit-radar').hidden);
});
test('the radar range and ring spacing agree with their distance (ru)', async (t) => {
const { $, cockpit } = fixture(t);
t.after(() => setLocale('en'));
setLocale('ru');
cockpit.enter();
await settle();
const shown = () => [
$('#cockpit-radar-range').textContent.trim(),
$('.cockpit-radar-rings').textContent.trim(),
];
// The ring spacing is a fraction at the short ranges: Intl.PluralRules
// gives 0.75 and 1.5 the form of their own.
const expected = [
['3 МОР. МИЛИ', 'КОЛЬЦА 0,75 МОР. МИЛИ'],
['6 МОР. МИЛЬ', 'КОЛЬЦА 1,5 МОР. МИЛИ'],
['12 МОР. МИЛЬ', 'КОЛЬЦА 3 МОР. МИЛИ'],
['24 МОР. МИЛИ', 'КОЛЬЦА 6 МОР. МИЛЬ'],
];
for (const [index, labels] of expected.entries()) {
if (index) {
$('#cockpit-radar-range').click();
await settle();
}
assert.deepEqual(shown(), labels);
}
});
test('contact navigation does not jump ship from the bridge', async (t) => {
const { cockpit, navigations } = fixture(t);
cockpit.enter();
@@ -1229,18 +1201,6 @@ test('a ship with no true heading is held, and labelled, on its course', async (
signals.some(([, , , detail]) => /COURSE 095°/.test(detail)),
JSON.stringify(signals),
);
// A ship's course has a word of its own: pt RUMO, an aircraft's CURSO.
t.after(() => setLocale('en'));
setLocale('pt');
signals.length = 0;
cockpit.exit();
await settle();
cockpit.enter();
await settle();
assert.ok(
signals.some(([, , , detail]) => /· RUMO 095°$/.test(detail)),
JSON.stringify(signals),
);
});
test('both entries name the subject the same way', async () => {
+2 -5
View File
@@ -1,5 +1,5 @@
/** Instrument values, routes and vision controls for the Cockpit controller. */
import { formatNumber, t, tn } from '../i18n/index.js';
import { formatNumber, t } from '../i18n/index.js';
import {
COCKPIT_VISION_MODES,
normalizeCockpitVisionMode,
@@ -379,10 +379,7 @@ export function updateRadar(info) {
);
this.publishRadar({
...picture,
// A plural, as the ring spacing is: ru «3 МОР. МИЛИ», «12 МОР. МИЛЬ».
rangeLabel: tn(rangeNm, '{range} NM', '{range} NM', {
range: formatNumber(rangeNm),
}),
rangeLabel: t('{range} NM', { range: formatNumber(rangeNm) }),
});
}
-8
View File
@@ -13,7 +13,6 @@ import { createPinia } from 'pinia';
import { createApp, defineComponent, h, nextTick } from 'vue';
import * as Cesium from 'cesium';
import { CockpitViewController } from './cockpitController.js';
import { setLocale, t as translate } from '../i18n/index.js';
import { useCockpitStore } from './stores/cockpit.js';
import { useHudStore } from './stores/hud.js';
import CockpitView from './views/CockpitView.vue';
@@ -535,13 +534,6 @@ test('a site row turns the camera onto it, holds it, and reads its off-nadir ang
assert.match(held.className, /^track actionable$/);
assert.match(held.detail, /^FOCUS · BASE · OFF-NADIR/);
assert.equal(siteRow(scene, 'alpha').getAttribute('aria-label'), 'Release the camera from Camp Alpha');
// The held site's FOCUS is a status, worded apart from the FOCUS command.
t.after(() => setLocale('en'));
setLocale('es');
await refreshRows(scene);
const heldEs = rows(scene).find((row) => row.target?.id === SITES.alpha.id);
assert.match(heldEs.detail, /^ENFOCADO · /);
assert.equal(translate('FOCUS'), 'ENFOCAR', 'the command keeps its verb');
});
test('another site row switches the focus; the held site row ends it', async (t) => {
+1 -2
View File
@@ -132,8 +132,7 @@ function siteRows(info, { settled = true } = {}) {
}),
});
const words = [installationClassLabel(site.class), angle];
// A status, not the FOCUS command: es «ENFOCADO», not «ENFOCAR».
if (focused) words.unshift(t('FOCUS', { context: 'site status' }));
if (focused) words.unshift(t('FOCUS'));
return {
key: `site:${site.id}`,
// The held site reads as the subject does.
-14
View File
@@ -7,7 +7,6 @@ import assert from 'node:assert/strict';
import { createPinia } from 'pinia';
import { createApp, defineComponent, h, nextTick } from 'vue';
import { CockpitViewController } from './cockpitController.js';
import { setLocale } from '../i18n/index.js';
import { useCockpitStore } from './stores/cockpit.js';
import CockpitView from './views/CockpitView.vue';
import ContextView from './views/ContextView.vue';
@@ -256,16 +255,3 @@ test('entering asks for a panel layout before Vue shows the cards', async (t) =>
await new Promise((resolve) => setTimeout(resolve, 50));
assert.deepEqual(laid, ['context', 'signals']);
});
test('an aircraft is acquired on its CURSO in Portuguese; a ship keeps RUMO', async (t) => {
const { cockpit } = fixture(t);
t.after(() => setLocale('en'));
setLocale('pt');
const signals = [];
cockpit.pushCockpitSignal = (...args) => signals.push(args);
cockpit.enter();
await nextTick();
const acquired = signals.find(([key]) => key === 'track');
assert.ok(acquired, JSON.stringify(signals));
assert.equal(acquired[3], 'UAL123 · CURSO 092°');
});
+1 -3
View File
@@ -326,11 +326,9 @@ export function enter({ kind = null } = {}) {
'track',
t('TRACK ACQUIRED'),
// A ship with no true heading is held on its course over ground; one
// with neither is named without a direction. A ship's course has a
// word of its own: pt «RUMO», where an aircraft's is «CURSO».
// with neither is named without a direction.
info.trackSource === 'course'
? t('{label} · COURSE {heading}°', {
context: 'vessel',
label: info.name,
heading: trackHeading,
})
+11 -34
View File
@@ -1,6 +1,6 @@
<script setup>
import { computed } from 'vue';
import { formatNumber, t, tn } from '../../i18n/index.js';
import { t } from '../../i18n/index.js';
import UiButton from '../kit/UiButton.vue';
import UiEmpty from '../kit/UiEmpty.vue';
import UiSelect from '../kit/UiSelect.js';
@@ -63,15 +63,9 @@ const categoryOptions = computed(() => [
const windowOptions = computed(() =>
(props.view?.windowChoices ?? []).map((hours) => ({
value: String(hours),
label: lastHours(hours),
label: t('Last {hours} h', { hours }),
})),
);
// The window offers 1 h, so every hour count is a plural: es «Última 1 h».
function lastHours(hours) {
return tn(hours, 'Last {hours} h', 'Last {hours} h', {
hours: formatNumber(hours),
});
}
// The longest choice is the server's whole window, kept as no choice.
const chooseWindow = (hours) =>
setFilter('hours', hours === props.view?.windowChoices?.[0] ? null : hours);
@@ -115,16 +109,7 @@ const clear = () => props.onParams({ clear: true });
<section
v-if="view"
class="news-readout"
:aria-label="
tn(
view.windowHours,
'News, last {hours} hour',
'News, last {hours} hours',
{
hours: formatNumber(view.windowHours),
},
)
"
:aria-label="t('News, last {hours} hours', { hours: view.windowHours })"
>
<UiSelect
v-if="windowOptions.length > 1"
@@ -135,7 +120,7 @@ const clear = () => props.onParams({ clear: true });
@update:model-value="chooseWindow(Number($event))"
/>
<p v-else class="news-readout__window ui-mono">{{
lastHours(view.windowHours)
t('Last {hours} h', { hours: view.windowHours })
}}</p>
<div
class="news-readout__sections"
@@ -211,12 +196,9 @@ const clear = () => props.onParams({ clear: true });
v-else-if="!view.inView.items.length && !view.inView.loading"
class="news-readout__empty"
:title="
tn(
view.windowHours,
'Nothing reported near here in the last {hours} h',
'Nothing reported near here in the last {hours} h',
{ hours: formatNumber(view.windowHours) },
)
t('Nothing reported near here in the last {hours} h', {
hours: view.windowHours,
})
"
/>
<ol
@@ -241,9 +223,7 @@ const clear = () => props.onParams({ clear: true });
class="news-readout__section"
:aria-label="t('Latest news')"
>
<h3 class="news-readout__head ui-mono">{{
t('Latest', { context: 'news list' })
}}</h3>
<h3 class="news-readout__head ui-mono">{{ t('Latest') }}</h3>
<UiEmpty
v-if="view.key"
class="news-readout__empty"
@@ -267,12 +247,9 @@ const clear = () => props.onParams({ clear: true });
v-else-if="!view.loading && !view.error"
class="news-readout__empty"
:title="
tn(
view.windowHours,
'No news matches these filters in the last {hours} h',
'No news matches these filters in the last {hours} h',
{ hours: formatNumber(view.windowHours) },
)
t('No news matches these filters in the last {hours} h', {
hours: view.windowHours,
})
"
/>
<div class="news-readout__footer">
-50
View File
@@ -1,7 +1,6 @@
import '../testSupport/registerDom.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { setLocale, t as translate } from '../i18n/index.js';
import { createNewsPanel } from './newsPanel.js';
import {
NEWS_CATEGORIES,
@@ -386,55 +385,6 @@ test('the window choice offers shorter windows; the longest is the server window
assert.deepEqual(calls.at(-1)[1], { filters: { hours: null } });
});
test('the time window agrees with its number of hours', (t) => {
t.after(() => setLocale('en'));
setLocale('es');
const { news, panel } = fixture();
news.update(view({ windowHours: 24, windowChoices: [24, 12, 6, 3, 1] }));
const choice = panel.querySelector(
'select.news-readout__window-choice, .news-readout__window-choice select',
);
assert.deepEqual(
[...choice.options].map((option) => option.textContent),
['Últimas 24 h', 'Últimas 12 h', 'Últimas 6 h', 'Últimas 3 h', 'Última 1 h'],
);
news.update(view({ windowHours: 1, items: [], hasMore: false }));
assert.equal(
panel.querySelector('.news-readout__window').textContent,
'Última 1 h',
);
assert.equal(
panel.querySelector('.news-readout').getAttribute('aria-label'),
'Noticias de la última 1 hora',
);
assert.match(
panel.textContent,
/En la última 1 h no hay noticias que coincidan con estos filtros/,
);
setLocale('ru');
news.update(view({ windowHours: 1, items: [], hasMore: false }));
assert.equal(
panel.querySelector('.news-readout').getAttribute('aria-label'),
'Новости за последний 1 час',
);
news.update(view({ windowHours: 12, items: [], hasMore: false }));
assert.equal(
panel.querySelector('.news-readout').getAttribute('aria-label'),
'Новости за последние 12 часов',
);
news.destroy();
});
test('the Latest news heading is worded apart from the timeline button (fr)', (t) => {
t.after(() => setLocale('en'));
setLocale('fr');
const { news, panel } = fixture();
news.update(view());
assert.equal(panel.querySelector('.news-readout__head').textContent, 'Dernières');
assert.equal(translate('Latest'), 'Dernière', 'the timeline keeps its own');
news.destroy();
});
test('the panel names the window the server serves', () => {
const { news, panel } = fixture();
news.update(view({ windowHours: 12 }));
+221
View File
@@ -0,0 +1,221 @@
import { useStatusStore } from './stores/status.js';
import { applicationPinia } from './vueRuntime.js';
import { describeError } from '../app/errors.js';
import { appRootUrl } from '../share/shareLinkPath.js';
/**
* Tell the user when the globe stops drawing, rather than leave a black globe
* behind controls that still respond. Cesium stops its render loop after a
* render error, and a page whose WebGL context was lost never draws again.
*
* Retry restarts the loop, and the notice stays until a frame renders without
* an error; after a context loss Retry reloads the page. Reset opens the app's
* root address. With `allowReload: false`, Reset is left out, and so is Retry
* after a context loss. Returns a function that removes the notice.
*/
export function installRenderFailureNotice(
viewer,
{
store = useStatusStore(applicationPinia()),
locationRef = globalThis.location,
allowReload = true,
logError = (error) =>
console.error('[Render] The globe stopped drawing:', error),
} = {},
) {
const { scene } = viewer;
const canvas = viewer.canvas ?? scene.canvas;
let notice = null;
let contextLost = false;
// The error Scene raised as a render error and, with rethrowRenderErrors,
// throws on: the render() wrapper must not raise it a second time.
let raisedByScene = null;
let frameFailed = false;
let removeFrameListeners = null;
function retry() {
if (contextLost) {
locationRef.reload();
return;
}
viewer.useDefaultRenderLoop = true;
scene.requestRender?.();
}
function reset() {
locationRef.assign(appRootUrl(locationRef.href));
}
function stopWatchingFrames() {
removeFrameListeners?.();
removeFrameListeners = null;
}
function clear() {
stopWatchingFrames();
if (store.renderFailure === notice) store.publishRenderFailure(null);
notice = null;
}
// Cesium raises postRender after a failed frame too, so only a frame that
// starts and ends without a render error clears the notice.
function watchFrames() {
if (removeFrameListeners || contextLost) return;
const removeStart = scene.preUpdate.addEventListener(() => {
frameFailed = false;
raisedByScene = null;
});
const removeEnd = scene.postRender.addEventListener(() => {
if (!frameFailed) clear();
});
removeFrameListeners = () => {
removeStart();
removeEnd();
};
}
function present(detail) {
const reason = contextLost ? 'context-lost' : 'render-error';
// A loop restarted into the same error keeps the notice as it was.
if (notice?.reason === reason && notice.detail === detail) return;
notice = {
reason,
detail,
retry: contextLost && !allowReload ? null : retry,
reset: allowReload ? reset : null,
};
store.publishRenderFailure(notice);
watchFrames();
}
const onRenderError = (_scene, error) => {
raisedByScene = { error };
frameFailed = true;
logError(error);
// A lost context fails every frame after it; the notice keeps saying why.
if (contextLost) return;
present(error ? describeError(error) : '');
};
const onContextLost = () => {
contextLost = true;
// No frame can clear this one; only a reload draws again.
stopWatchingFrames();
present('');
};
const removeRenderError = scene.renderError.addEventListener(onRenderError);
// An error thrown outside Scene's own error handling (a preRender or
// postRender listener, the clock's onTick, a resize) also stops Cesium's
// render loop, but raises no renderError, and Cesium reports it only in the
// panel the app turns off. Raise it as a render error, so Cesium's handler,
// an inline panel's timer and this notice all see it. Such an error passes
// here before anything else runs, so finish the raises of frameEvents() it
// cut short first: listeners added or removed while the loop is stopped
// then apply at once, in order.
const unguard = ['resize', 'render'].map((name) =>
reportThrown(viewer.cesiumWidget, name, (error) => {
frameEvents(viewer).forEach(finishRaise);
if (!raisedByScene || raisedByScene.error !== error)
scene.renderError.raiseEvent(scene, error);
raisedByScene = null;
}),
);
canvas?.addEventListener?.('webglcontextlost', onContextLost);
return () => {
removeRenderError();
unguard.forEach((restore) => restore());
canvas?.removeEventListener?.('webglcontextlost', onContextLost);
clear();
};
}
/**
* The events Cesium raises in a frame outside Scene's own error handling, so
* that a listener's error cuts their raise short: Scene's frame events, the
* camera's and the clock's, and those raised after the frame renders, the
* globe's tile load progress and the load events of the scene's tilesets.
* Tilesets nested in a collection are not reached.
*/
function frameEvents(viewer) {
const { scene } = viewer;
const events = [
scene.preUpdate,
scene.postUpdate,
scene.preRender,
scene.postRender,
scene.camera?.changed,
scene.camera?.moveStart,
scene.camera?.moveEnd,
viewer.clock?.onTick,
viewer.clock?.onStop,
scene.globe?.tileLoadProgressEvent,
];
const primitives = scene.primitives;
for (let i = 0; i < (primitives?.length ?? 0); i += 1) {
const tileset = primitives.get(i);
events.push(
tileset?.loadProgress,
tileset?.allTilesLoaded,
tileset?.initialTilesLoaded,
tileset?.tileLoad,
);
}
return events;
}
/**
* Finish the raise a throwing listener cut short. Cesium's `Event.raiseEvent`
* marks the event as raising until every listener returns, and until then it
* only queues the listeners added or removed. Apply the queue as the raise
* would have, so listeners added or removed from here on take effect at once.
*/
function finishRaise(event) {
if (!event?._invokingListeners) return;
event._invokingListeners = false;
for (const [listener, scopes] of event._toAdd) {
const added = event._listeners.get(listener) ?? new Set();
scopes.forEach((scope) => added.add(scope));
event._listeners.set(listener, added);
}
event._toAdd.clear();
for (const [listener, scopes] of event._toRemove) {
const left = event._listeners.get(listener);
scopes.forEach((scope) => left?.delete(scope));
if (left?.size === 0) event._listeners.delete(listener);
}
event._toRemove.clear();
}
/**
* Report an error the widget's `name` method throws, and throw it on.
*
* A render error marks Cesium's render loop stopped, but a call made while
* the loop ran leaves the loop's next animation frame queued: the loop queues
* it after a call that returns, and a frame an inline panel's timer draws
* leaves it queued. That frame stops the loop itself, and a Retry or a shown
* tab that comes first must not start a second loop beside it, so the loop
* stays marked running. A call from the loop that throws is still marked
* stopped by the loop's own catch.
*/
function reportThrown(widget, name, report) {
const original = widget?.[name];
if (typeof original !== 'function') return () => {};
const own = Object.hasOwn(widget, name);
const guarded = function (...args) {
const looping = widget._renderLoopRunning;
try {
return original.apply(this, args);
} catch (error) {
report(error);
throw error;
} finally {
if (looping) widget._renderLoopRunning = true;
}
};
widget[name] = guarded;
return () => {
if (widget[name] !== guarded) return;
if (own) widget[name] = original;
else delete widget[name];
};
}
+187
View File
@@ -0,0 +1,187 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { installRenderFailureNotice } from './renderFailure.js';
function event() {
const listeners = new Set();
return {
listeners,
addEventListener(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
raise(...args) {
for (const listener of [...listeners]) listener(...args);
},
};
}
function fixture(t, options = {}) {
const canvasListeners = new Map();
const scene = {
renderError: event(),
preUpdate: event(),
postRender: event(),
requestRender() {
this.requests = (this.requests ?? 0) + 1;
},
};
const viewer = {
scene,
useDefaultRenderLoop: true,
canvas: {
addEventListener: (name, listener) => canvasListeners.set(name, listener),
removeEventListener: (name, listener) => {
if (canvasListeners.get(name) === listener)
canvasListeners.delete(name);
},
},
};
const store = {
renderFailure: null,
published: [],
publishRenderFailure(next) {
this.renderFailure = next;
this.published.push(next);
},
};
const visits = [];
const locationRef = {
href: 'https://gev.example/share/AQc?lang=ru#lat=48.8584&lon=2.2945',
reload: () => visits.push('reload'),
assign: (url) => visits.push(String(url)),
};
const logged = [];
const uninstall = installRenderFailureNotice(viewer, {
store,
locationRef,
logError: (error) => logged.push(error),
...options,
});
t.after(uninstall);
// One Cesium frame: preUpdate, an optional render error, then postRender,
// which Cesium raises after a failed frame too.
const frame = (error) => {
scene.preUpdate.raise(scene);
if (error) {
viewer.useDefaultRenderLoop = false;
scene.renderError.raise(scene, error);
}
scene.postRender.raise(scene);
};
return {
viewer,
scene,
store,
visits,
logged,
uninstall,
frame,
loseContext: () => canvasListeners.get('webglcontextlost')?.({}),
canvasListeners,
};
}
test('a render error shows a notice that survives the failed frame', (t) => {
const f = fixture(t);
assert.equal(f.store.renderFailure, null);
const error = new Error('Expected width to be greater than 0');
f.frame(error);
const notice = f.store.renderFailure;
assert.equal(notice.reason, 'render-error');
assert.equal(notice.detail, 'Expected width to be greater than 0');
assert.equal(typeof notice.retry, 'function');
assert.equal(typeof notice.reset, 'function');
assert.deepEqual(f.logged, [error]);
});
test('Retry restarts the render loop and a clean frame clears the notice', (t) => {
const f = fixture(t);
f.frame(new Error('boom'));
f.store.renderFailure.retry();
assert.equal(f.viewer.useDefaultRenderLoop, true);
assert.equal(f.scene.requests, 1);
assert.ok(f.store.renderFailure, 'the notice waits for a frame');
f.frame();
assert.equal(f.store.renderFailure, null);
// Clearing stops the per-frame listeners.
assert.equal(f.scene.preUpdate.listeners.size, 0);
assert.equal(f.scene.postRender.listeners.size, 0);
});
test('a loop restarted into the same error keeps one notice', (t) => {
const f = fixture(t);
f.frame(new Error('boom'));
const first = f.store.renderFailure;
f.store.renderFailure.retry();
f.frame(new Error('boom'));
assert.equal(f.store.renderFailure, first);
assert.equal(f.store.published.length, 1);
f.frame(new Error('other'));
assert.equal(f.store.renderFailure.detail, 'other');
assert.ok(!f.store.published.includes(null), 'no new appearance');
});
test('a new failure after recovery is a new appearance', (t) => {
const f = fixture(t);
f.frame(new Error('boom'));
f.store.renderFailure.retry();
f.frame();
f.frame(new Error('boom'));
assert.deepEqual(
f.store.published.map((notice) => notice?.reason ?? null),
['render-error', null, 'render-error'],
);
});
test('a render error without a value shows no detail', (t) => {
const f = fixture(t);
f.viewer.useDefaultRenderLoop = false;
f.scene.renderError.raise(f.scene, undefined);
assert.equal(f.store.renderFailure.reason, 'render-error');
assert.equal(f.store.renderFailure.detail, '');
});
test('Reset opens the app root without the restored view', (t) => {
const f = fixture(t);
f.frame(new Error('boom'));
f.store.renderFailure.reset();
assert.deepEqual(f.visits, ['https://gev.example/?lang=ru']);
});
test('a lost context offers a reload and no frame clears it', (t) => {
const f = fixture(t);
f.loseContext();
const notice = f.store.renderFailure;
assert.equal(notice.reason, 'context-lost');
assert.equal(notice.detail, '');
// Cesium's next frame then fails on the dead drawing buffer.
f.frame(new Error('Expected width to be greater than 0'));
assert.equal(f.store.renderFailure, notice, 'the notice keeps saying why');
f.frame();
assert.ok(f.store.renderFailure, 'a lost context never draws again');
f.store.renderFailure.retry();
assert.deepEqual(f.visits, ['reload']);
});
test('a page that must not reload offers neither reload nor reset', (t) => {
const f = fixture(t, { allowReload: false });
f.frame(new Error('boom'));
assert.equal(typeof f.store.renderFailure.retry, 'function');
assert.equal(f.store.renderFailure.reset, null);
f.loseContext();
assert.equal(f.store.renderFailure.retry, null);
assert.equal(f.store.renderFailure.reset, null);
});
test('uninstall removes the listeners and the notice', (t) => {
const f = fixture(t);
f.frame(new Error('boom'));
f.uninstall();
assert.equal(f.store.renderFailure, null);
assert.equal(f.scene.renderError.listeners.size, 0);
assert.equal(f.scene.postRender.listeners.size, 0);
assert.equal(f.canvasListeners.size, 0);
f.frame(new Error('later'));
assert.equal(f.store.renderFailure, null);
});
+259
View File
@@ -0,0 +1,259 @@
// Retry after an error thrown by a scene event listener. Cesium's
// Event.raiseEvent has no try/finally: a throwing listener leaves the event
// with _invokingListeners = true, so listeners added or removed afterwards are
// only queued (_toAdd/_toRemove) until the next raise of that event completes.
// Drives Cesium's real Event and startRenderLoop (CesiumWidget.prototype).
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { installRenderFailureNotice } from './renderFailure.js';
function setup() {
const frames = [];
const previousRaf = globalThis.requestAnimationFrame;
globalThis.requestAnimationFrame = (fn) => frames.push(fn);
// Scene.render as in Scene.js:4369: preUpdate always, preRender/postRender
// only when a frame is rendered (requestRenderMode idle skips them).
const scene = {
renderError: new Cesium.Event(),
preUpdate: new Cesium.Event(),
preRender: new Cesium.Event(),
postRender: new Cesium.Event(),
requestRenderMode: true,
_renderRequested: false,
rendered: 0,
requestRender() {
this._renderRequested = true;
},
initializeFrame() {},
render(time) {
this.preUpdate.raiseEvent(this, time);
const shouldRender = !this.requestRenderMode || this._renderRequested;
if (!shouldRender) return;
this._renderRequested = false;
this.preRender.raiseEvent(this, time);
this.rendered += 1;
this.postRender.raiseEvent(this, time);
},
};
const widget = Object.create(Cesium.CesiumWidget.prototype);
Object.assign(widget, {
_scene: scene,
_clock: { tick() {} },
_canRender: true,
_useDefaultRenderLoop: false,
_renderLoopRunning: false,
_showRenderLoopErrors: false,
_targetFrameRate: undefined,
isDestroyed: () => false,
resize() {},
});
scene.renderError.addEventListener(() => {
widget._useDefaultRenderLoop = false;
widget._renderLoopRunning = false;
});
const viewer = {
scene,
cesiumWidget: widget,
canvas: { addEventListener() {}, removeEventListener() {} },
get useDefaultRenderLoop() {
return widget.useDefaultRenderLoop;
},
set useDefaultRenderLoop(value) {
widget.useDefaultRenderLoop = value;
},
};
const store = {
renderFailure: null,
publishRenderFailure(next) {
this.renderFailure = next;
},
};
const uninstall = installRenderFailureNotice(viewer, {
store,
locationRef: { href: 'https://gev.example/', reload() {}, assign() {} },
logError() {},
});
const runFrames = (n) => {
for (let i = 0; i < n; i += 1)
for (const fn of frames.splice(0)) fn(16 * (i + 1));
};
const restore = () => {
uninstall();
globalThis.requestAnimationFrame = previousRaf;
};
return { frames, scene, widget, viewer, store, runFrames, restore };
}
for (const eventName of ['postRender', 'preUpdate']) {
test(`idle: a ${eventName} listener that threw once; a successful Retry clears the notice`, () => {
const { scene, viewer, store, runFrames, restore } = setup();
try {
let broken = true;
scene[eventName].addEventListener(() => {
if (broken) throw new TypeError('listener failed');
});
scene.requestRender();
viewer.useDefaultRenderLoop = true;
runFrames(1);
assert.equal(store.renderFailure?.reason, 'render-error');
assert.equal(viewer.useDefaultRenderLoop, false);
broken = false; // the cause goes away
store.renderFailure.retry();
const before = scene.rendered;
runFrames(10); // idle: no camera move, no render request
assert.ok(scene.rendered > before, 'the globe drew again');
assert.equal(store.renderFailure, null, 'the notice is cleared');
} finally {
restore();
}
});
}
test('a throwing postRender listener removed before Retry is not called again', () => {
const { scene, viewer, store, runFrames, restore } = setup();
try {
let calls = 0;
const remove = scene.postRender.addEventListener(() => {
calls += 1;
throw new TypeError('layer listener failed');
});
scene.requestRender();
viewer.useDefaultRenderLoop = true;
runFrames(1);
assert.equal(store.renderFailure?.reason, 'render-error');
remove(); // the user turns the layer off
store.renderFailure.retry();
runFrames(5);
assert.equal(calls, 1, 'the removed listener ran again after Retry');
assert.equal(viewer.useDefaultRenderLoop, true, 'the loop keeps running');
assert.equal(store.renderFailure, null, 'the notice is cleared');
} finally {
restore();
}
});
const listenerCount = (event) =>
[...event._listeners.values()].reduce((sum, scopes) => sum + scopes.size, 0);
// While the loop is stopped, a layer turned off and on again, or a ground
// task in worldAnnotationRenderer that starts, times out and starts again.
const RESUBSCRIBE = {
'turned off and on': (event, listener) => {
event.removeEventListener(listener);
event.addEventListener(listener);
},
'added, removed and added again': (event, listener) => {
event.addEventListener(listener);
event.removeEventListener(listener);
event.addEventListener(listener);
},
};
for (const [name, resubscribe] of Object.entries(RESUBSCRIBE)) {
test(`a listener ${name} while the loop is stopped runs after Retry`, () => {
const { scene, viewer, store, runFrames, restore } = setup();
try {
let broken = true;
scene.postRender.addEventListener(() => {
if (broken) throw new TypeError('layer listener failed');
});
let calls = 0;
const listener = () => {
calls += 1;
};
if (name === 'turned off and on')
scene.postRender.addEventListener(listener);
scene.requestRender();
viewer.useDefaultRenderLoop = true;
runFrames(1);
assert.equal(store.renderFailure?.reason, 'render-error');
resubscribe(scene.postRender, listener);
broken = false;
store.renderFailure.retry();
calls = 0;
scene.requestRender();
runFrames(3);
assert.ok(calls > 0, 'the re-added listener runs after Retry');
assert.equal(
scene.postRender.numberOfListeners,
listenerCount(scene.postRender),
);
} finally {
restore();
}
});
}
// Every event a listener's error can cut short is finished when the error
// surfaces, so a listener added at once is subscribed.
const FRAME_EVENTS = {
'scene.preUpdate': (v) => v.scene.preUpdate,
'scene.postUpdate': (v) => v.scene.postUpdate,
'scene.preRender': (v) => v.scene.preRender,
'scene.postRender': (v) => v.scene.postRender,
'camera.changed': (v) => v.scene.camera.changed,
'camera.moveStart': (v) => v.scene.camera.moveStart,
'camera.moveEnd': (v) => v.scene.camera.moveEnd,
'clock.onTick': (v) => v.clock.onTick,
'clock.onStop': (v) => v.clock.onStop,
'globe.tileLoadProgressEvent': (v) => v.scene.globe.tileLoadProgressEvent,
'tileset.loadProgress': (v) => v.scene.primitives.get(0).loadProgress,
'tileset.allTilesLoaded': (v) => v.scene.primitives.get(0).allTilesLoaded,
'tileset.initialTilesLoaded': (v) =>
v.scene.primitives.get(0).initialTilesLoaded,
'tileset.tileLoad': (v) => v.scene.primitives.get(0).tileLoad,
};
for (const [name, pick] of Object.entries(FRAME_EVENTS)) {
test(`a listener that throws in ${name} leaves it finished`, () => {
const events = () => ({
changed: new Cesium.Event(),
moveStart: new Cesium.Event(),
moveEnd: new Cesium.Event(),
});
const tileset = {
loadProgress: new Cesium.Event(),
allTilesLoaded: new Cesium.Event(),
initialTilesLoaded: new Cesium.Event(),
tileLoad: new Cesium.Event(),
};
const viewer = {
scene: {
renderError: new Cesium.Event(),
preUpdate: new Cesium.Event(),
postUpdate: new Cesium.Event(),
preRender: new Cesium.Event(),
postRender: new Cesium.Event(),
camera: events(),
globe: { tileLoadProgressEvent: new Cesium.Event() },
primitives: { length: 1, get: () => tileset },
},
clock: { onTick: new Cesium.Event(), onStop: new Cesium.Event() },
canvas: { addEventListener() {}, removeEventListener() {} },
cesiumWidget: {
_renderLoopRunning: false,
render: () => pick(viewer).raiseEvent(),
},
};
const store = { publishRenderFailure() {} };
const uninstall = installRenderFailureNotice(viewer, {
store,
locationRef: { href: 'https://gev.example/', reload() {}, assign() {} },
logError() {},
});
try {
const event = pick(viewer);
event.addEventListener(() => {
throw new TypeError('listener failed');
});
assert.throws(() => viewer.cesiumWidget.render(), TypeError);
assert.equal(event._invokingListeners, false);
const listener = () => {};
event.addEventListener(listener);
assert.ok(event._listeners.has(listener));
} finally {
uninstall();
}
});
}
+93
View File
@@ -0,0 +1,93 @@
// Errors thrown in the render loop outside Scene's tryAndCatchError (a
// preRender/postRender listener, clock.onTick, resize). Drives Cesium's real
// startRenderLoop through the CesiumWidget.prototype.useDefaultRenderLoop
// setter.
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { installRenderFailureNotice } from './renderFailure.js';
// What src/app/scene.js passes to the viewer (showRenderLoopErrors).
const SHOW_RENDER_LOOP_ERRORS = false;
function runLoop(render) {
const frames = [];
const previousRaf = globalThis.requestAnimationFrame;
globalThis.requestAnimationFrame = (fn) => frames.push(fn);
try {
const scene = {
renderError: new Cesium.Event(),
preUpdate: new Cesium.Event(),
postRender: new Cesium.Event(),
requestRender() {},
initializeFrame() {},
render,
};
const widget = Object.create(Cesium.CesiumWidget.prototype);
Object.assign(widget, {
_scene: scene,
_clock: { tick() {} },
_canRender: true,
_useDefaultRenderLoop: false,
_renderLoopRunning: false,
_showRenderLoopErrors: SHOW_RENDER_LOOP_ERRORS,
_targetFrameRate: undefined,
isDestroyed: () => false,
resize() {},
showErrorPanel() {},
});
const viewer = {
scene,
cesiumWidget: widget,
canvas: { addEventListener() {}, removeEventListener() {} },
get useDefaultRenderLoop() {
return widget.useDefaultRenderLoop;
},
set useDefaultRenderLoop(value) {
widget.useDefaultRenderLoop = value;
},
};
const store = {
renderFailure: null,
publishRenderFailure(next) {
this.renderFailure = next;
},
};
const logged = [];
const uninstall = installRenderFailureNotice(viewer, {
store,
locationRef: { href: 'https://gev.example/', reload() {}, assign() {} },
logError: (error) => logged.push(error),
});
viewer.useDefaultRenderLoop = true;
while (frames.length) frames.shift()(16);
return { widget, store, logged, uninstall };
} finally {
globalThis.requestAnimationFrame = previousRaf;
}
}
test('an error thrown outside tryAndCatchError still shows the notice', () => {
// Scene.render raises preRender outside tryAndCatchError (Scene.js:4439),
// so a listener's exception propagates.
const { widget, store, logged, uninstall } = runLoop(function () {
this.preUpdate.raiseEvent(this);
throw new TypeError("Cannot read properties of undefined (reading 'x')");
});
assert.equal(widget.useDefaultRenderLoop, false);
assert.equal(store.renderFailure?.reason, 'render-error');
assert.equal(logged.length, 1);
uninstall();
});
test('an error Scene raised and threw on with rethrowRenderErrors is reported once', () => {
const { store, logged, uninstall } = runLoop(function () {
this.preUpdate.raiseEvent(this);
const error = new Error('boom');
this.renderError.raiseEvent(this, error);
throw error;
});
assert.equal(store.renderFailure?.detail, 'boom');
assert.equal(logged.length, 1);
uninstall();
});
+151
View File
@@ -0,0 +1,151 @@
// Retry after an error Scene catches itself (Scene.tryAndCatchError raises
// renderError and returns). CesiumWidget's renderError handler marks the loop
// stopped, but startRenderLoop still queues the loop's next animation frame.
// A Retry that lands before that frame runs must not start a second loop.
// Drives Cesium's real startRenderLoop and useDefaultRenderLoop setter
// (CesiumWidget.prototype).
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { installRenderFailureNotice } from './renderFailure.js';
function setup({ targetFrameRate } = {}) {
const frames = [];
const previousRaf = globalThis.requestAnimationFrame;
globalThis.requestAnimationFrame = (fn) => frames.push(fn);
const scene = {
renderError: new Cesium.Event(),
preUpdate: new Cesium.Event(),
preRender: new Cesium.Event(),
postRender: new Cesium.Event(),
broken: false,
drawn: 0,
requestRender() {},
initializeFrame() {},
// Scene.render (Scene.js:4369): an update error is caught by
// tryAndCatchError, which raises renderError and returns normally.
render(time) {
this.drawn += 1;
this.preUpdate.raiseEvent(this, time);
if (this.broken) this.renderError.raiseEvent(this, new Error('boom'));
this.preRender.raiseEvent(this, time);
this.postRender.raiseEvent(this, time);
},
};
const widget = Object.create(Cesium.CesiumWidget.prototype);
Object.assign(widget, {
_scene: scene,
_clock: { tick() {} },
_canRender: true,
_useDefaultRenderLoop: false,
_renderLoopRunning: false,
_showRenderLoopErrors: false,
_targetFrameRate: targetFrameRate,
isDestroyed: () => false,
resize() {},
});
// CesiumWidget's own renderError handler (CesiumWidget.js:414).
scene.renderError.addEventListener(() => {
widget._useDefaultRenderLoop = false;
widget._renderLoopRunning = false;
});
const viewer = {
scene,
cesiumWidget: widget,
canvas: { addEventListener() {}, removeEventListener() {} },
get useDefaultRenderLoop() {
return widget.useDefaultRenderLoop;
},
set useDefaultRenderLoop(value) {
widget.useDefaultRenderLoop = value;
},
};
const store = {
renderFailure: null,
publishRenderFailure(next) {
this.renderFailure = next;
},
};
const uninstall = installRenderFailureNotice(viewer, {
store,
locationRef: { href: 'https://gev.example/', reload() {}, assign() {} },
logError() {},
});
let time = 0;
const runFrames = (n) => {
for (let i = 0; i < n; i += 1) {
time += 17;
for (const fn of frames.splice(0)) fn(time);
}
};
const restore = () => {
uninstall();
globalThis.requestAnimationFrame = previousRaf;
};
return { frames, scene, viewer, store, runFrames, restore };
}
for (const targetFrameRate of [undefined, 60]) {
test(`targetFrameRate ${targetFrameRate}: Retry before the next frame after an error Scene caught keeps one render loop`, () => {
const { frames, scene, viewer, store, runFrames, restore } = setup({
targetFrameRate,
});
try {
viewer.useDefaultRenderLoop = true;
runFrames(2);
scene.broken = true;
runFrames(1);
assert.equal(store.renderFailure?.reason, 'render-error');
// The cause is gone; Retry runs (a queued Enter on the focused Retry)
// before the loop's next animation frame.
scene.broken = false;
store.renderFailure.retry();
assert.equal(frames.length, 1, 'one animation frame queued after Retry');
scene.drawn = 0;
runFrames(3);
assert.equal(store.renderFailure, null);
assert.equal(scene.drawn, 3, 'the globe draws once a frame');
} finally {
restore();
}
});
}
test('control: Retry after the next frame ran keeps one render loop', () => {
const { frames, scene, viewer, store, runFrames, restore } = setup();
try {
viewer.useDefaultRenderLoop = true;
runFrames(2);
scene.broken = true;
runFrames(1);
scene.broken = false;
runFrames(1);
assert.equal(frames.length, 0, 'the stopped loop queued nothing');
store.renderFailure.retry();
scene.drawn = 0;
runFrames(3);
assert.equal(scene.drawn, 3);
} finally {
restore();
}
});
test('a tab hidden and shown again before the next frame after an error Scene caught keeps one render loop', () => {
const { scene, viewer, runFrames, restore } = setup();
try {
viewer.useDefaultRenderLoop = true;
runFrames(2);
scene.broken = true;
runFrames(1);
scene.broken = false;
// src/app/tools.js turns the loop off while the tab is hidden and back on
// when it is shown, before the queued frame runs.
viewer.useDefaultRenderLoop = false;
viewer.useDefaultRenderLoop = true;
scene.drawn = 0;
runFrames(3);
assert.equal(scene.drawn, 3, 'the globe draws once a frame');
} finally {
restore();
}
});
+174
View File
@@ -0,0 +1,174 @@
// src/ui/renderFailureView.test.mjs — the scene chrome renders the render
// failure notice installRenderFailureNotice() publishes, with its actions.
import '../testSupport/registerDom.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createApp, nextTick } from 'vue';
import { createPinia } from 'pinia';
import { useStatusStore } from './stores/status.js';
import SceneChromeView from './views/SceneChromeView.vue';
function mount(t) {
document.body.replaceChildren();
const pinia = createPinia();
const app = createApp(SceneChromeView).use(pinia);
app.mount(document.body.appendChild(document.createElement('div')));
t.after(() => app.unmount());
return useStatusStore(pinia);
}
const notice = () => document.getElementById('render-failure');
const buttons = () =>
[...(notice()?.querySelectorAll('button') ?? [])].map((button) =>
button.textContent.trim(),
);
test('a render failure shows an alert dialog that moves focus to Retry', async (t) => {
const store = mount(t);
assert.equal(notice(), null, 'no notice while the globe draws');
const calls = [];
store.publishRenderFailure({
reason: 'render-error',
detail: 'Expected width to be greater than 0',
retry: () => calls.push('retry'),
reset: () => calls.push('reset'),
});
await nextTick();
await nextTick();
const element = notice();
assert.equal(element.getAttribute('role'), 'alertdialog');
assert.equal(
document.getElementById(element.getAttribute('aria-labelledby'))
.textContent,
'The globe stopped drawing',
);
assert.match(
document.getElementById(element.getAttribute('aria-describedby'))
.textContent,
/^Rendering stopped after an error\./,
);
assert.equal(
element.querySelector('.render-failure-detail').textContent,
'Expected width to be greater than 0',
);
assert.deepEqual(buttons(), ['Retry', 'Reset view']);
assert.equal(document.activeElement?.textContent.trim(), 'Retry');
for (const button of element.querySelectorAll('button')) button.click();
assert.deepEqual(calls, ['retry', 'reset']);
store.publishRenderFailure(null);
await nextTick();
assert.equal(notice(), null);
});
test('a lost context offers Reload, and a panel that cannot reload offers nothing', async (t) => {
const store = mount(t);
store.publishRenderFailure({
reason: 'context-lost',
detail: '',
retry: () => {},
reset: () => {},
});
await nextTick();
assert.match(
document.getElementById('render-failure-message').textContent,
/3D graphics/,
);
assert.equal(notice().querySelector('.render-failure-detail'), null);
assert.deepEqual(buttons(), ['Reload', 'Reset view']);
store.publishRenderFailure({
reason: 'context-lost',
detail: '',
retry: null,
reset: null,
});
await nextTick();
assert.deepEqual(buttons(), []);
assert.equal(notice().querySelector('.render-failure-hint'), null);
});
const failure = () => ({
reason: 'render-error',
detail: '',
retry: () => {},
reset: null,
});
const settle = async () => {
await nextTick();
await nextTick();
};
test('focus goes back where it was when the globe draws again', async (t) => {
const store = mount(t);
const search = document.body.appendChild(document.createElement('input'));
search.focus();
store.publishRenderFailure(failure());
await settle();
assert.equal(document.activeElement?.textContent.trim(), 'Retry');
store.publishRenderFailure(null);
await settle();
assert.equal(document.activeElement, search);
});
// The globe's canvas, as CesiumWidget builds it inside the container.
const addGlobeCanvas = () => {
const widget = document.createElement('div');
widget.className = 'cesium-widget';
document.getElementById('cesiumContainer').appendChild(widget);
return widget.appendChild(document.createElement('canvas'));
};
test('focus goes to the globe canvas when the element it came from is gone', async (t) => {
const store = mount(t);
const canvas = addGlobeCanvas();
const search = document.body.appendChild(document.createElement('input'));
search.focus();
store.publishRenderFailure(failure());
await settle();
search.remove();
store.publishRenderFailure(null);
await settle();
// The map's keys, Enter to finish a drawing among them, read the canvas
// as the map (src/annotations/drawTool.js).
assert.equal(document.activeElement, canvas);
assert.equal(canvas.getAttribute('tabindex'), '-1');
canvas.blur();
assert.equal(canvas.hasAttribute('tabindex'), false, 'only this once');
});
test('focus goes to the globe canvas when the element it came from cannot take it', async (t) => {
const store = mount(t);
const canvas = addGlobeCanvas();
const button = document.body.appendChild(document.createElement('button'));
button.focus();
store.publishRenderFailure(failure());
await settle();
button.disabled = true;
store.publishRenderFailure(null);
await settle();
assert.equal(document.activeElement, canvas);
// A press on the globe leaves focus on the page again, as Cesium has it.
canvas.dispatchEvent(new Event('pointerdown'));
assert.equal(document.activeElement, document.body);
assert.equal(canvas.hasAttribute('tabindex'), false);
});
test('focus the user moved while the notice was up stays put', async (t) => {
const store = mount(t);
store.publishRenderFailure(failure());
await settle();
const chat = document.body.appendChild(document.createElement('textarea'));
chat.focus();
store.publishRenderFailure(null);
await settle();
assert.equal(document.activeElement, chat);
});
test('the globe drawing from the start leaves focus alone', async (t) => {
mount(t);
await settle();
assert.equal(document.activeElement, document.body);
assert.equal(
document.getElementById('cesiumContainer').hasAttribute('tabindex'),
false,
);
});
+17 -1
View File
@@ -7,7 +7,9 @@ import { markRaw, shallowRef } from 'vue';
* road network chip, CctvControls the camera grid chip; the toast shows
* what any owner's createToast() (src/ui/toast.js) publishes, and the title
* bar the FPS readout createFrameRateMonitor() publishes. Each label keeps its
* last text while hidden, so the next change flaps from what was shown.
* last text while hidden, so the next change flaps from what was shown. The
* render failure notice is what installRenderFailureNotice()
* (src/ui/renderFailure.js) publishes while the globe cannot draw.
*/
export const useStatusStore = defineStore('gev-status', () => {
/**
@@ -32,6 +34,13 @@ export const useStatusStore = defineStore('gev-status', () => {
*/
const fps = shallowRef(null);
/**
* The globe's render failure, `{reason, detail, retry, reset}`; null while
* the globe draws. `reason` is 'render-error' or 'context-lost', and `retry`
* and `reset` are null where that action is not offered.
*/
const renderFailure = shallowRef(null);
/** @param {?object} next A fresh presentation of the loading status. */
function publishLoading(next) {
loading.value = next && markRaw(next);
@@ -57,6 +66,11 @@ export const useStatusStore = defineStore('gev-status', () => {
fps.value = next && markRaw(next);
}
/** @param {?object} next A fresh presentation of the render failure. */
function publishRenderFailure(next) {
renderFailure.value = next && markRaw(next);
}
return {
fps,
publishFps,
@@ -68,5 +82,7 @@ export const useStatusStore = defineStore('gev-status', () => {
publishTraffic,
cctv,
publishCctv,
renderFailure,
publishRenderFailure,
};
});
+70
View File
@@ -405,6 +405,76 @@ html:not([lang='en']) #global-loading-detail {
}
}
/* ── Render failure notice ───────────────────── */
/* Above the loading screen, so a globe that fails while it loads says so too,
and above floating panels such as the Québec camera viewer (2000). */
#render-failure {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2001;
box-sizing: border-box;
width: min(420px, calc(100vw - 32px));
max-height: calc(100vh - 32px);
overflow-y: auto;
padding: 20px 22px;
background: var(--glass-bg);
border: 1px solid #ff7272;
border-radius: 12px;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
box-shadow: 0 0 24px rgba(255, 88, 88, 0.18);
color: var(--text-primary);
font-family: var(--font-sans);
font-size: 13px;
line-height: 1.5;
}
#render-failure-title {
margin: 0 0 8px;
font-family: var(--font-mono);
font-size: 12px;
font-weight: 600;
letter-spacing: 2px;
text-transform: uppercase;
color: #ff7272;
}
#render-failure p {
margin: 0 0 10px;
}
#render-failure .render-failure-detail {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-secondary);
overflow-wrap: anywhere;
}
#render-failure .render-failure-hint {
font-size: 12px;
color: var(--text-secondary);
}
#render-failure .render-failure-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
#render-failure .gev-action-button {
font-size: 11px;
padding: 8px 14px;
}
@media (pointer: coarse) {
#render-failure .gev-action-button {
min-height: 44px;
}
}
/* ── Toast Notification ──────────────────────── */
#toast {
position: fixed;
+2
View File
@@ -18,6 +18,7 @@ import ScreenAnnotations from './panels/ScreenAnnotations.vue';
import AwarenessDirection from './panels/AwarenessDirection.vue';
import UiButton from '../kit/UiButton.vue';
import GlobeActions from './panels/GlobeActions.vue';
import RenderFailure from './panels/RenderFailure.vue';
import WorldTimeline from './panels/WorldTimeline.vue';
// The ACTIVE STYLE corner shows the style VisualSettings publishes.
@@ -61,6 +62,7 @@ const vToastText = {
<template>
<div id="cesiumContainer"></div>
<RenderFailure />
<div id="world-overlay-root" aria-hidden="true">
<canvas id="world-overlay-canvas"></canvas>
</div>
+1 -3
View File
@@ -118,11 +118,9 @@ const ringFractions = computed(() =>
? radar.value.ringsNm.map((ring) => ring / radar.value.rangeNm)
: [],
);
// The spacing is a fraction at the shorter ranges (0.75, 1.5 NM), which
// Intl.PluralRules gives a form of its own: ru «0,75 МОР. МИЛИ».
const ringLabel = computed(() =>
radar.value?.ringsNm.length
? tn(radar.value.ringsNm[0], 'RINGS {spacing} NM', 'RINGS {spacing} NM', {
? t('RINGS {spacing} NM', {
spacing: formatNumber(radar.value.ringsNm[0]),
})
: '',
@@ -163,20 +163,6 @@ test('filters narrow the list as the viewer types and picks', async (t_) => {
assert.deepEqual(shownIds(), ['nasa-eonet-events']);
});
test('the shown count agrees with its number (he)', async (t_) => {
const { owner } = mount(t_);
setLocale('he');
owner.open();
await settle();
const status = () => $('.data-library-status').textContent.trim();
assert.match(status(), /^מוצגים\s+\S*3\S*\s+מתוך/);
const search = $('#data-library input[type="search"]');
search.value = 'eonet';
search.dispatchEvent(new Event('input'));
await settle();
assert.match(status(), /^מוצג\s+\S*1\S*\s+מתוך/);
});
test('Add becomes Remove on the same focused button; Zoom closes first', async (t_) => {
const { owner, library } = mount(t_);
owner.open();
+5 -11
View File
@@ -1,7 +1,7 @@
<script setup>
import { storeToRefs } from 'pinia';
import { computed, nextTick, useTemplateRef, watch } from 'vue';
import { formatNumber, t, tn } from '../../../i18n/index.js';
import { t, tn } from '../../../i18n/index.js';
import UiButton from '../../kit/UiButton.vue';
import UiSelect from '../../kit/UiSelect.js';
import {
@@ -53,18 +53,12 @@ watch(
const tiles = computed(() => libraryStatTiles(props.view.summary));
const themes = computed(() => libraryThemeOptions());
// The words for the shown entries agree with their count: he «מוצג 1».
const status = computed(() =>
props.view.ready
? tn(
props.view.shown.length,
'Showing {shown} of {total}',
'Showing {shown} of {total}',
{
shown: formatNumber(props.view.shown.length),
total: formatNumber(props.view.total),
},
)
? t('Showing {shown} of {total}', {
shown: props.view.shown.length,
total: props.view.total,
})
: t('Checking what this instance serves…'),
);
const resetText = computed(() => {
+91
View File
@@ -0,0 +1,91 @@
<script setup>
import { nextTick, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { msg, t } from '../../../i18n/index.js';
import { useStatusStore } from '../../stores/status.js';
import UiButton from '../../kit/UiButton.vue';
// The notice installRenderFailureNotice() (src/ui/renderFailure.js) publishes
// while the globe cannot draw, with the actions it offers.
const { renderFailure: failure } = storeToRefs(useStatusStore());
const panel = ref(null);
const MESSAGES = {
'context-lost': msg(
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.",
),
'render-error': msg(
'Rendering stopped after an error. Layers and controls still work, but the map is not updating.',
),
};
// Focus moves to the first action each time the notice appears, so a
// keyboard user can act on it at once. When the globe draws again, it goes
// back where it was, or to the map if that cannot take it, unless the user
// has moved it since.
let focusBefore;
watch(
() => Boolean(failure.value),
async (shown) => {
if (shown) {
focusBefore = document.activeElement;
await nextTick();
panel.value?.querySelector('button')?.focus();
return;
}
if (focusBefore === undefined) return;
const previous = focusBefore;
focusBefore = undefined;
await nextTick();
const active = document.activeElement;
if (active && active !== document.body) return;
if (previous?.isConnected && previous !== document.body) {
previous.focus({ preventScroll: true });
// A control disabled, hidden or made inert since does not take it.
if (document.activeElement === previous) return;
}
focusMap();
},
{ immediate: true },
);
// The globe's canvas, which the map's keys (Enter to finish a drawing, Space
// to talk) read as the map. It takes focus only this once: when focus moves
// on, or the globe is pressed, it goes back to a canvas that cannot take
// focus, and a press leaves focus on the page, as Cesium has it.
function focusMap() {
const canvas = document.querySelector(
'#cesiumContainer .cesium-widget canvas',
);
if (!canvas) return;
if (!canvas.hasAttribute('tabindex')) {
canvas.tabIndex = -1;
const done = new AbortController();
const release = () => {
done.abort();
canvas.removeAttribute('tabindex');
};
canvas.addEventListener('blur', release, { signal: done.signal });
canvas.addEventListener(
'pointerdown',
() => {
release();
canvas.blur();
},
{ capture: true, signal: done.signal },
);
}
canvas.focus({ preventScroll: true });
}
</script>
<template>
<div v-if="failure" id="render-failure" ref="panel" role="alertdialog" aria-labelledby="render-failure-title" aria-describedby="render-failure-message">
<h2 id="render-failure-title">{{ t('The globe stopped drawing') }}</h2>
<p id="render-failure-message">{{ t(MESSAGES[failure.reason] ?? MESSAGES['render-error']) }}</p>
<p v-if="failure.detail" class="render-failure-detail" translate="no">{{ failure.detail }}</p>
<p v-if="failure.reset" class="render-failure-hint">{{ t('Reset view reloads the app without the view restored from the address bar.') }}</p>
<div v-if="failure.retry || failure.reset" class="render-failure-actions">
<UiButton v-if="failure.retry" class="gev-action-button render-failure-retry" :onClick="failure.retry">{{ failure.reason === 'context-lost' ? t('Reload') : t('Retry') }}</UiButton>
<UiButton v-if="failure.reset" class="gev-action-button render-failure-reset" :onClick="failure.reset">{{ t('Reset view') }}</UiButton>
</div>
</div>
</template>
-2
View File
@@ -3917,8 +3917,6 @@ async function requestStreetLevel(
};
// Then the place: a name that geocodes to nothing changes nothing else.
// "the view centre" is English for the model; the card (speech.js) gives
// it messages of its own, since it is a phrase and not a place name.
label =
query ||
(entity ? String(entity.label || entity.id) : '') ||
+11 -32
View File
@@ -332,16 +332,6 @@ const ERROR_PATTERNS = [
place: localizePlaceName(place),
}),
],
// show_street_level names no place when it looks at the view centre. The
// phrase is not a name: it has a message of its own, so each language
// words it in the case its preposition asks for.
[
/^No (.+) imagery near the view centre$/,
([, source]) =>
t('No {source} imagery near the view centre', {
source: translateKnown(source),
}),
],
[
/^No (.+) imagery near (.+)$/,
([, source, place]) =>
@@ -1122,21 +1112,14 @@ export function planStepLabel(name, args = {}) {
}
case 'show_street_level': {
const source = STREET_LEVEL_SOURCE_NAMES[args.provider];
// "This spot" and "the view centre" are phrases, not place names:
// each is a message of its own, worded in the grammar of the language.
if (args.query) {
const place = shownPlace(args.query, 32);
return source
? t('{source} at {place}', { source, place })
: t('Street level at {place}', { place });
}
if (args.latitude != null)
return source
? t('{source} at this spot', { source })
: t('Street level at this spot');
const place = args.query
? shownPlace(args.query, 32)
: args.latitude != null
? t('this spot')
: t('the view centre');
return source
? t('{source} at the view centre', { source })
: t('Street level at the view centre');
? t('{source} at {place}', { source, place })
: t('Street level at {place}', { place });
}
case 'set_layer_visibility': {
const on = args.enabled !== false;
@@ -1212,9 +1195,6 @@ export function narrationLabel(name, args = {}) {
}
}
/** The place show_street_level reports when it names none (gevActions). */
const VIEW_CENTRE_LABEL = 'the view centre';
/** On-screen labels for progress steps. */
const STEP_LABELS = Object.freeze({
resolve: msg('Finding places'),
@@ -1239,11 +1219,10 @@ export function progressStepLabel(step, label) {
: step
? spokenLabel(step, 24)
: t('Working');
// The join is a catalog string so each language spaces its colon. The
// view centre is a phrase show_street_level reports in English, not a name.
const state =
label === VIEW_CENTRE_LABEL ? t('the view centre') : spokenLabel(label, 32);
return label ? t('{name}: {state}', { name: base, state }) : base;
// The join is a catalog string so each language spaces its colon.
return label
? t('{name}: {state}', { name: base, state: spokenLabel(label, 32) })
: base;
}
/**
+1 -49
View File
@@ -10,7 +10,7 @@ import {
} from './speech.js';
import { presentResult } from './resultDisplay.js';
import { countWords, findHedges } from './speechLint.js';
import { setLocale, t } from '../i18n/index.js';
import { setLocale } from '../i18n/index.js';
function envelope(name, result, args) {
return attachVoiceResult(name, result, args);
@@ -779,51 +779,3 @@ test('progress step labels join through the catalog colon', () => {
setLocale('en');
}
});
test('the street-level card words "the view centre" and "this spot" as phrases, not names', () => {
setLocale('de');
try {
assert.equal(
planStepLabel('show_street_level', { provider: 'mapillary' }),
'Mapillary in der Bildmitte',
);
assert.equal(
planStepLabel('show_street_level', { latitude: 1, longitude: 2 }),
'Straßenebene an dieser Stelle',
);
assert.equal(
planStepLabel('show_street_level', { query: 'Alexanderplatz', provider: 'kartaview' }),
'KartaView: Alexanderplatz',
);
const empty = envelope('show_street_level', {
ok: false,
stage: 'imagery',
noImagery: true,
atViewCentre: true,
label: 'the view centre',
providerName: 'Mapillary',
error: 'No Mapillary imagery near the view centre',
});
assert.deepEqual(empty.display.lines, [
'Keine Bilder von Mapillary in der Nähe der Bildmitte',
]);
assert.equal(empty.say, 'No Mapillary imagery near the view centre.');
const named = envelope('show_street_level', {
ok: false,
stage: 'imagery',
noImagery: true,
label: 'Alexanderplatz',
providerName: 'Mapillary',
error: 'No Mapillary imagery near Alexanderplatz',
});
assert.deepEqual(named.display.lines, [
'Keine Bilder von Mapillary in der Nähe von Alexanderplatz',
]);
assert.equal(
progressStepLabel('imagery', 'the view centre'),
`${t('Opening imagery')}: die Bildmitte`,
);
} finally {
setLocale('en');
}
});