Compare commits

..
Author SHA1 Message Date
Mazeyar Moeini FeizabadiandClaude Fable 5.2 d4f86748ea Merge the Transit provider fix and current main; fix the Bike Share test harness
The provider/globe identity check from #1688 moves into the shared terrain
reader, so every layer gets it; Transit shows the reader the flat provider
with observe(), since it answers a flat globe itself.

The Bike Share height test kept one array entry per event-loop turn and
spread it into Math.max, which overflowed the stack on slow CI runners. It
now keeps a running maximum.

Both found in review by kvnloo.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-13 03:27:30 +01:00
Mazeyar Moeini Feizabadi c29ee98e23 Revert "style(transit): format the terrain provider test"
This reverts commit 5110f4cce3.
2026-11-13 03:26:17 +01:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 5110f4cce3 style(transit): format the terrain provider test
Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-13 03:25:07 +01:00
Mazeyar Moeini Feizabadi 8792e4701e Merge remote-tracking branch 'origin/main' into mazeyar/transit-terrain-height-cache 2026-11-13 03:22:10 +01:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 87674240bd fix(transit): start terrain heights afresh when the provider or globe changes
A height read after the camera and tiles settled was kept until either
changed, but swapping the terrain provider or the globe with a still camera
changed neither, so the previous terrain's elevation could stand
indefinitely. The cache is now cleared when either identity changes, checked
before the flat-globe answer so terrain -> flat -> terrain is covered too.

Found in review by kvnloo.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-13 03:22:10 +01:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 888662749a docs(changelog): name the shared terrain fragment after its pull request
Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-12 23:05:39 +01:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 0f4dda5cc6 perf(terrain): one shared, cached and budgeted terrain height reader for Transit, Rail, Bike Share and local data
globe.getHeight is a ray cast against the drawn tile's mesh and
scene.sampleHeight a GPU depth render; several layers asked for them
uncached and unbudgeted. The cache written for Transit moves to
src/services/terrainSurface.js and is handed to every layer through the
ground service, so layers share one cache and one read budget.

- Rail reads each train on a ~4 m grid instead of every frame.
- Local data layers read terrain floors through it and stop depth samples
  at 8 ms a frame.
- Bike Share draws stations at once and finds their heights afterwards in
  4 ms steps.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-12 23:05:24 +01:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 d5f7926c7e docs(changelog): name the transit height fragment after its pull request
Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-12 21:50:22 +01:00
Mazeyar Moeini FeizabadiandClaude Fable 5.2 1c49a89faa fix(transit): keep terrain heights per point so rereads stop freezing the page
On a terrain globe every floor reread asked globe.getHeight, a ray cast against
the drawn tile's mesh, for every trail fix of every vehicle in view, twice per
pass and seven passes per poll. Over Boston's 650 vehicles that was about
150,000 reads per 10 s for about 8,000 distinct points, and blocked the page
for up to 19 s at a time while zooming.

Heights are now kept per point (1e-5 degrees), read again only while the camera
moves or tiles load, and read within a time budget: vehicle positions first,
the path a marker is drawn along next, undrawn trail history last.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-12 21:10:49 +01:00
79 changed files with 2866 additions and 2269 deletions

No files matched your search

+2 -4
View File
@@ -31,7 +31,6 @@ How to read this:
| **adsb.lol** | Military flights + aircraft traces | ODbL 1.0 | "adsb.lol" (ODbL) |
| **adsbdb** (`api.adsbdb.com`) | Aircraft type, model name and registration by ICAO hex for enriched flights; airline and origin/destination airports for the tracked flight's callsign | No licence or rate limit is published for the service. Its own credits: aircraft data from PlaneBase, ICAO-to-N-number conversion by Guillaume Michel, and flight route data that "is the work of David Taylor, Edinburgh and Jim Mason, Glasgow, and may not be copied, published, or incorporated into other databases without the explicit permission of David J Taylor, Edinburgh" | "adsbdb — api.adsbdb.com" (courtesy); route data credited to David Taylor, Edinburgh, and Jim Mason, Glasgow |
| **Planespotters.net** (`api.planespotters.net/pub/photos/hex/{hex}`) | One thumbnail photo of the tracked aircraft, by ICAO hex, in the CONTEXT panel's CONTACTS view under the tracked flight's name | [Planespotters photo API terms](https://www.planespotters.net/photo/api); each photo remains the copyright of its photographer. Thumbnails may be hotlinked on the condition that the photographer is named next to the photo and the photo links to its Planespotters page with a followable link | "Photo © {photographer}" beside every photo that names one, linked to its Planespotters page; "Planespotters.net" in Data attribution |
| **Open Vessel Photos** (`openvesselphotos.org/v1/mmsi/{mmsi}`) | One photo of the selected vessel, by MMSI, in the CONTEXT panel's CONTACTS view under the vessel's name and MMSI | Free, keyless public API ([README](https://github.com/louptheron/openvesselphotos)); each photo keeps the open licence its author chose (CC BY, CC BY-SA, CC0, or public domain), given with it in the API | "Photo: {photographer} · {licence}" beside every photo, linked to the vessel's Open Vessel Photos page; "Open Vessel Photos" in Data attribution |
| **AISStream.io** | Live vessels (AIS) | Free, beta, no formal ToS; AIS is a public broadcast | "AISStream.io" (courtesy) |
| **CelesTrak** | Satellite TLEs (SGP4) and SATCAT records (object type, operational status, owner) that file each satellite under a class | US-government-origin data, no license; citation requested | "CelesTrak (celestrak.org), Dr. T.S. Kelso" |
| **The Space Devs — Launch Library 2 v2.3** | Recent launch, payload, stage, and recovery metadata for Space Missions (30d) | [The Space Devs terms of use](https://github.com/TheSpaceDevs/Tutorials/blob/main/faqs/faq_TSD.md#terms-of-use): data may be used and shared in any form; avoid forwarding it without added value; attribution is encouraged (not mandatory). [Official API limits](https://ll.thespacedevs.com/docs/): 15 unauthenticated calls/hour; optional token | "Launch Library 2 — The Space Devs" (courtesy attribution) |
@@ -239,8 +238,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
- **adsbdb enrichment.** `/api/adsbdb` resolves an ICAO hex to aircraft type, model name and registration, and the tracked flight's callsign to an airline and an origin/destination pair. Fan-out is deliberately small because adsbdb is a free community API that publishes no quota: at most 4 requests in flight, dispatches dripped to roughly 5/s, and each key asked for at most once per session (for the flights layer's enrichment, a lookup that failed temporarily, such as an outage, a timeout, a rate limit or an authorization refusal that may clear, may be asked again after a 60-second cool-down; an answer, found or not, and a permanent refusal such as a 404 are final; Local ADS-B lookups are asked once), with route lookups reserved for the tracked aircraft. The dev-server proxy caches each key for 24 hours in `.gev-cache/adsbdb.json`, negative answers included, so fresh entries survive server restarts. That gitignored cache remains local to the deployment and is not bundled with this repo. adsbdb publishes no general service license; its README says the route data may not be copied, published, or incorporated into other databases without the explicit permission of David J Taylor, Edinburgh. Deployers are responsible for obtaining any permission their use requires. Enrichment is best-effort and fail-silent — a flight that gets no answer keeps what the flight feed already gave it.
- **Planespotters photos.** When you track an aircraft, the browser asks `https://api.planespotters.net/pub/photos/hex/{hex}` directly (the API answers cross-origin, so no key and no server proxy are involved) for that aircraft only, once per hex per session, through the same enrichment queue as adsbdb, with an 8-second timeout. A miss is remembered for the session; a request that failed temporarily (an outage, the timeout or a rate limit) may be asked again after a 60-second cool-down while the aircraft stays tracked, as for the flights layer's other enrichment. Either way no photo shows meanwhile. The first photo whose image is an `https://` URL on `*.plnspttrs.net`, and whose page is an `https://www.planespotters.net` URL is shown in the CONTEXT panel's CONTACTS view, under the tracked flight's name (`src/ui/views/panels/AwarenessPanel.vue`), and nowhere else; a vessel (see Open Vessel Photos below), an installation, a military aircraft or a converted TR-3B has none, and a military aircraft or TR-3B is never looked up. Anything else is dropped, so the app never shows or links outside Planespotters (`src/data/planespottersPhoto.js`, also exported as `gods-eye-view/sources/planespotters` for servers that cache the lookup). The photo follows the attribution rules: the thumbnail is hotlinked from the URL the API gives (never copied or re-hosted), the photographer's name, when the answer gives one, is visible text next to it ("Photo © {photographer}"; a photo whose answer names no one shows no credit line), and the photo and credit are a plain link to the photo's Planespotters page with `target="_blank" rel="noopener"` and never `rel="nofollow"`. If the image fails to load, the photo and its credit are hidden.
- **Open Vessel Photos.** When you select a vessel with a nine-digit MMSI, the browser asks `https://openvesselphotos.org/v1/mmsi/{mmsi}` directly (the API is keyless and answers cross-origin, so no key and no server proxy are involved; account credentials are never sent) for that vessel only, once per MMSI per session, with an 8-second timeout. A miss (a 404 or a vessel without photos) is remembered for the session; a failed request may be asked again after five minutes. The first photo whose image is an `https://openvesselphotos.org` URL and which names a licence is shown, at the 800 px size, in the CONTEXT panel's CONTACTS view under the vessel's name and MMSI (`src/ui/views/panels/AwarenessPanel.vue`), and nowhere else (`src/data/openVesselPhoto.js`, `src/layers/vessels/photos.js`). The image is hotlinked from the URL the API gives (never copied or re-hosted); the photographer and licence are visible text next to it ("Photo: {photographer} · {licence}", or "Photo · {licence}" when the answer names no one), and the photo and credit link to the vessel's Open Vessel Photos page with `target="_blank" rel="noopener"`. If the image fails to load, the photo and its credit are hidden.
- **Planespotters photos.** When you track an aircraft, the browser asks `https://api.planespotters.net/pub/photos/hex/{hex}` directly (the API answers cross-origin, so no key and no server proxy are involved) for that aircraft only, once per hex per session, through the same enrichment queue as adsbdb, with an 8-second timeout. A miss is remembered for the session; a request that failed temporarily (an outage, the timeout or a rate limit) may be asked again after a 60-second cool-down while the aircraft stays tracked, as for the flights layer's other enrichment. Either way no photo shows meanwhile. The first photo whose image is an `https://` URL on `*.plnspttrs.net`, and whose page is an `https://www.planespotters.net` URL is shown in the CONTEXT panel's CONTACTS view, under the tracked flight's name (`src/ui/views/panels/AwarenessPanel.vue`), and nowhere else; a vessel, an installation, a military aircraft or a converted TR-3B has none, and a military aircraft or TR-3B is never looked up. Anything else is dropped, so the app never shows or links outside Planespotters (`src/data/planespottersPhoto.js`, also exported as `gods-eye-view/sources/planespotters` for servers that cache the lookup). The photo follows the attribution rules: the thumbnail is hotlinked from the URL the API gives (never copied or re-hosted), the photographer's name, when the answer gives one, is visible text next to it ("Photo © {photographer}"; a photo whose answer names no one shows no credit line), and the photo and credit are a plain link to the photo's Planespotters page with `target="_blank" rel="noopener"` and never `rel="nofollow"`. If the image fails to load, the photo and its credit are hidden.
- **Launch Library 2.** `/api/launches` makes a server-side rolling-30-day query against the supported v2.3 detailed launch endpoint, caches successful responses for 15 minutes in memory and on disk, and serves the last successful response during a throttle or transient outage. Anonymous access is limited to 15 calls/hour; deployments can provide `LL2_API_TOKEN` for authenticated access. The Space Devs' published terms permit using and sharing the API data in any form, ask users not to forward it without adding value, disclaim complete accuracy, and encourage—but do not require—attribution. This app keeps a courtesy credit. Payload and stage/recovery records are shown only when supplied. Failed launches expose their source status and never receive fallback orbit geometry or a live/estimated marker. LL2 supplies launch context and event timing, not continuous ascent telemetry or live orbital state.
- **TfL JamCams.** The camera list comes from the keyless `api.tfl.gov.uk` endpoint (an optional `TFL_APP_KEY` raises its rate limit); frames come from TfL's public S3 bucket. The "Powered by TfL Open Data" attribution is required by TfL's terms and is registered in the Data attribution popover.
- **Ontario 511 CCTV.** The camera list comes from the `511on.ca/api/v2/get/cameras` endpoint, which now requires a free developer key (`ONTARIO_511_API_KEY`: sign up at 511on.ca/my511/register, then request it while logged in at 511on.ca/developers/doc; throttled to 10 calls a minute). Without the key the pack is skipped. Frames are server-registered still images under `511on.ca/map/Cctv/`. The source is live-fetched at runtime and attributed under the Open Government Licence - Ontario. Access is governed by Ontario 511's Developer's Access Agreement (511on.ca/developers/resources): the data stays under the Open Government Licence - Ontario unless otherwise specified; key requests are reviewed case by case and ask for the intended use and any third parties that will receive the data; attribution by link to 511on.ca is encouraged, and the Ontario 511 logo may not be altered or used to imply endorsement (this app links and shows no logo); and the data is provided "as is", a disclaimer distributors should pass on, which the credit does.
@@ -688,4 +686,4 @@ NOAA density imagery with the raw GLM product.
## In-app attribution
The required Google Maps / Cesium credit renders on the on-globe credit line (`#cesium-credits`, bottom-left) and must stay visible — including in clean-view and recording modes (the whole line, logo + "Google Maps" + the "Data attribution" link, stays on screen; only the GEV panels/HUD fade). The layer-specific credits (adsb.lol, adsbdb, Planespotters.net, Open Vessel Photos, TeleGeography, OSM datacenters/dams/roads, NASA FIRMS, CelesTrak, USGS, City of Austin, Fintraffic, GBFS, Radio Browser, OpenSky, AISStream) are registered into the expandable **"Data attribution"** popover on that credit line via `viewer.creditDisplay.addStaticCredit(new Cesium.Credit(html, /* showOnScreen */ false))` — see `src/data/dataCredits.js`. When you add a new data source, add its license and attribution to this file **and** append an entry to `DATA_CREDITS` in `src/data/dataCredits.js` so it surfaces in the app.
The required Google Maps / Cesium credit renders on the on-globe credit line (`#cesium-credits`, bottom-left) and must stay visible — including in clean-view and recording modes (the whole line, logo + "Google Maps" + the "Data attribution" link, stays on screen; only the GEV panels/HUD fade). The layer-specific credits (adsb.lol, adsbdb, Planespotters.net, TeleGeography, OSM datacenters/dams/roads, NASA FIRMS, CelesTrak, USGS, City of Austin, Fintraffic, GBFS, Radio Browser, OpenSky, AISStream) are registered into the expandable **"Data attribution"** popover on that credit line via `viewer.creditDisplay.addStaticCredit(new Cesium.Credit(html, /* showOnScreen */ false))` — see `src/data/dataCredits.js`. When you add a new data source, add its license and attribution to this file **and** append an entry to `DATA_CREDITS` in `src/data/dataCredits.js` so it surfaces in the app.
@@ -0,0 +1,7 @@
- Fix the page freezing for seconds at a time with Transit on over a terrain
globe, most visibly while zooming over a large fleet. Every reread asked the
terrain for the height of every trail fix of every vehicle in view; heights
are now kept per point, read again only while the camera moves or tiles
load, and read within a time budget that puts vehicle positions first.
Switching the terrain provider or globe starts the kept heights afresh
(mazy1998, review by kvnloo, #1688).
@@ -0,0 +1,5 @@
- Read terrain heights through one shared, cached and budgeted reader
(`src/services/terrainSurface.js`) in Transit, Rail, Bike Share and the local
data layers. Opening Data Centers no longer blocks the page for over two
seconds, opening Bike Share for most of a second, and Rail asks the terrain
about a twentieth as often (mazy1998, #1689).
-2
View File
@@ -1,2 +0,0 @@
- Show a new CCTV camera's frame the moment it is decoded, with no fade, so
switching cameras no longer lingers on an empty preview (samehkhamis, #1693).
@@ -1,3 +0,0 @@
- End a CCTV camera's monitor plane where its view meets the ground on globe
maps too, instead of a kilometre-wide plane hundreds of metres up
(samehkhamis, #1694).
-3
View File
@@ -1,3 +0,0 @@
- Show a selected vessel's photo from Open Vessel Photos, credited with its
photographer and open licence, and its MMSI beside its name in the CONTACTS
view (samehkhamis, #1695).
-3
View File
@@ -1,3 +0,0 @@
- Add an `./i18n` entry so an application with its own language menu can
start, follow and switch the interface language as the globe picker does
(samehkhamis, #1696).
@@ -1,5 +0,0 @@
- Fix the translations of the strings added since the last review (News,
Data Library, Wildlife Sightings, Shipwrecks, Rail, the vessel and orbital
cockpits) in all twelve languages: neutral side labels and counts no longer
break the sentence, terms follow each glossary, and Traditional Chinese
follows Simplified Chinese again (German, @gekh, #1698).
+1 -3
View File
@@ -224,9 +224,7 @@ leaving out `command-dock` the voice control and its pointer reticle;
leaving out `context` the Bhote Koshi reconstruction panel; and leaving out
`intel-hud` the Intel HUD overlay (`#intel-hud`), whose readouts the HUD
implementation publishes to the `gev-hud` store. An unknown
view name is an error. An application that offers its own language choice
starts and switches the language with the `./i18n` entry (`startPageLocale`,
`followPageLocale`, `switchLocale`; see [I18N.md](./I18N.md)) and passes
view name is an error. A host that does not initialize Core's locale passes
`localePicker: false` to `createApplicationControls` instead of editing the
markup. Node tests that import application modules register the component
loader with `node --import=<core package>/build/register-vue`.
+34 -20
View File
@@ -2224,9 +2224,7 @@ Frame loading, calibration editing and status display have separate modules;
providers, camera placement and navigation policy retain their existing owners.
Changing cameras invalidates old image callbacks and cancels an unfinished
calibration edit. Failed refreshes preserve settled pixels for the same camera,
while a newly selected camera never shows the prior camera's image. A frame is
shown without a fade once it is decoded (or after one second if its decode never
settles), so a new camera's preview is blank only while its frame loads. Disposal
while a newly selected camera never shows the prior camera's image. Disposal
releases listeners, subscriptions, image handlers, summary timers and chip-hide timers.
## UI disposal
@@ -3201,15 +3199,6 @@ stays in view or tracked; an answer, found or not, and a permanent refusal
stay final for the session. A route belongs to the callsign it was looked up
for, so a callsign change drops the old route. See `DATA_SOURCES.md`.
A selected vessel gets a photo the same way. The browser asks Open Vessel
Photos for it by MMSI (`fetchOpenVesselPhoto` in
`src/sources/live/standalone.js`, read by `src/data/openVesselPhoto.js`), once
per MMSI (`src/layers/vessels/photos.js`), and the CONTACTS view shows it under
the vessel's name and MMSI ("NAME · MMSI 366999709") with the photographer,
the photo's open licence and a link to the vessel's page. A vessel without an
MMSI is never looked up; a miss stays final for the session, and a failed
lookup is asked again after five minutes.
`gods-eye-view/server/providers/live` is a Node-only entry for the existing
plugins and shared request helpers. Importing it starts no sockets or timers.
The existing aircraft normalizer is separately available through the portable
@@ -5080,10 +5069,6 @@ Shipped locales: **en** (source and fallback), **ar**, **de**, **es**, **fr**, *
views re-render, code that published translated text words it again through
`onLocaleChange`, and `<html lang>`/`dir` and the page's `data-i18n` markup
follow. Nothing is lost; a toast already on screen keeps its words.
An application that offers the language elsewhere (a menu) uses the
`./i18n` entry (`src/i18n/page.js`): `startPageLocale` at boot,
`followPageLocale` for the markup and `switchLocale`, which does what a
picker choice does, with `localePicker: false`.
- **Right to left.** Arabic (`ar`) and Hebrew (`he`) are in `RTL_LOCALES`, so
`<html dir="rtl">`: the title, style indicator, both panel stacks and their
icon rails swap sides through logical properties (`inset-inline-*`,
@@ -5821,6 +5806,39 @@ Noir samples also exclude its deliberately vignetted outer field, using shader
settings and screen position rather than observed brightness. Selected-trail
diagnostics report every condition even when selection fails.
Heights of the terrain globe's loaded surface come from one shared reader,
`src/services/terrainSurface.js`, which the ground service hands to every layer
(`groundFloor.terrainSurface`). `globe.getHeight` is a ray cast against the
drawn tile's mesh, so the reader keeps each answer per point, about a metre
apart. While the camera moves or tiles load a kept height is read again after
2 s; a height read once both have been still for 5 s is kept until either
changes. Reads are budgeted at 16 ms per 200 ms window across all layers, 12 ms
of it for re-reads, longest-kept first. A request ranks as a marker (always
read), drawn geometry, or background (a quarter of each budget). Past the
budget a kept point keeps its last height and a new point takes the last
height read in its ~111 m floor cell or waits.
- Transit asks for every vehicle and trail fix: vehicle positions as markers,
the path a marker is drawn along next, the undrawn trail history of
unselected vehicles as background, and its reread cycle continues until no
point waits. Before this, every reread cast a ray for every trail fix of
every vehicle in view, which blocked the page for up to 19 s at a time over
a 650-vehicle fleet while zooming.
- Rail asks for each train every frame on a grid about 4 m apart, as a marker,
so a moving train reads a few times a second and a standing one not at all.
- Local data layers (Data Centers, Dams) read terrain floors through it, and
their depth samples (`scene.sampleHeight`, a GPU render) stop at 8 ms a
frame per layer and continue on the next frames.
- Bike Share draws a city's stations at once at the fallback height and finds
their ground heights afterwards in 4 ms steps, from the terrain reader on a
terrain globe and from depth samples where the globe is hidden.
Verified in the browser on a keyless terrain globe only: Boston for Transit,
Rail and Bike Share, northern Virginia for Data Centers. Not checked there:
photorealistic 3D Tiles, a terrain globe with a building tileset on top, Dams,
and any city steeper than Boston. Street Traffic keeps its own height cache
and ALPR cameras their own sample budget.
Transit brackets use the normal-composite overlay surface: a 1.25 px mode-colour
stroke over 3.25 px dark backing. Other contact themes retain their existing
behavior. The selected trail uses a 3 px core over 5 px backing, mode-coloured
@@ -6585,10 +6603,6 @@ silently demoting every later lookup for the session.
far cap, retargeting the existing video/canvas texture pipeline. Stills are drawn at their source aspect, centered on the placeholder dark (`#01070c`), instead of being stretched to the 16:9 canvas. Manual calibration only —
auto-calibration and the drape mesh pipeline are deleted. A one-shot activation obstruction
probe (`pickFromRay` on camera activation, clamping the plane short of the first hit) remains;
a hit at or beyond the camera's range counts as a miss, and on a globe map the plane also clamps
short of the globe's terrain along the axis (flat ground at the mount's ground when terrain is
missed or met beyond the range), whichever is nearer, so it never stands at the full range there
(about a kilometre wide and hundreds of metres up);
ground placement is superseded by the shared-floor v3 behavior below. Calibration persists to
`godsEyeView.cctv.calibration.v2` (wiped clean, no v1 import); a panel-only CAL badge shows
`CALIBRATED`/`CURATED`/`RAW PRIOR` (no in-world tint). Panel is titled "CCTV" (not "CCTV
-25
View File
@@ -59,30 +59,6 @@ The language picker switches in place, without a reload. A view that calls `t()`
A toast already on screen keeps its words.
## An application with its own language choice
An application that composes Core and offers the language somewhere else (a menu, a settings page) imports the `./i18n` entry (`src/i18n/page.js`) and passes `localePicker: false` to `createApplicationControls`:
```js
import {
LOCALES,
LOCALE_NAMES,
followPageLocale,
getLocale,
onLocaleChange,
startPageLocale,
switchLocale,
} from '<core package>/i18n';
startPageLocale(document); // before any view renders: ?lang=, stored choice, browser, English; translates data-i18n markup
const stopFollowing = followPageLocale(document); // <html lang>/dir and data-i18n markup follow each switch
for (const code of LOCALES) addMenuItem(LOCALE_NAMES[code], code === getLocale());
menu.onChoose = (code) => switchLocale(code); // switched in place and stored (in ?lang when storage is blocked)
```
`switchLocale` does what a choice in the globe picker does, and returns `'unchanged'` for the active language. `onLocaleChange` tells the application's own menu to mark the new language.
## Rules
- **Pass literals.** `t('Close')`, not `t(label)` (unless `label` holds a `msg()` value) and never a template literal with `${…}`.
@@ -133,7 +109,6 @@ Icons (`aria-hidden="true"`, Material Symbols), comments, CSS values and lowerca
| `src/ui/i18n.js` | the page's vue-i18n instance: the active locale and the catalogs |
| `src/i18n/marker.js` | `msg` alone, for modules the server or tools import (no catalogs) |
| `src/i18n/locale.js` | shipped languages, their names, choosing the language at boot |
| `src/i18n/page.js` | the `./i18n` entry: start, follow and switch the page's language |
| `src/i18n/locales/<code>.json` | one catalog per language, keyed by English text |
| `src/i18n/same-as-english.json` | per language, the entries that stay as in English on purpose |
| `scripts/i18n.mjs` | `npm run i18n`: extracts messages, checks the catalogs, finds unmarked UI text |
+1
View File
@@ -34,6 +34,7 @@ const layers = createInfrastructureLayers({
| `clearSelectedEntityContextForLayer` | `(layerId)`; clear selection only if owned by this layer |
| `removeEntityContextsForLayer` | `(layerId)`; remove records owned by the layer on destruction or failed setup |
| `governorRequestRender` | `(reason)`; request a frame through the existing render scheduler |
| `terrainSurface` | Optional. The application's shared terrain surface (`groundFloor.terrainSurface`), so its layers read each terrain height once; without it each layer keeps its own |
Each returned layer supports `init(viewer)`, `enable(viewer)`, `disable(viewer)`,
`update(viewer)`, `destroy(viewer)`, `getStats()`, `getLodDiagnostics()` and
+1 -1
View File
@@ -4,7 +4,7 @@ Catalog: `src/i18n/locales/zh-Hant.json`. General rules: [TRANSLATORS.md](../TRA
`zh-Hant` is the Simplified Chinese catalog (`zh-CN`, credited to abiyeenzo, #914, and EDIT-Aeterna, #979) written in Traditional characters with OpenCC character conversion. Wording and terminology are the same as in `zh-CN`: change a term in [zh-CN.md](./zh-CN.md) and the `zh-CN` catalog first, then convert. Only the script differs.
After `s2tw` conversion, fix the characters OpenCC picks wrongly: 年曆史 → 年歷史, 下遊 → 下游, 並道 → 併道, 發佈 → 發布, 公佈 → 公布, 控制檯 → 控制台, and 周 → 週 where it means a week (1 週/秒).
After `s2tw` conversion, fix the characters OpenCC picks wrongly: 年曆史 → 年歷史, 下遊 → 下游, 並道 → 併道, 發佈 → 發布, 公佈 → 公布.
## Style
-1
View File
@@ -99,7 +99,6 @@
"./application": "./src/app/application.js",
"./application/viewer": "./src/app/viewer.js",
"./time": "./src/time/worldClock.js",
"./i18n": "./src/i18n/page.js",
"./build/vite": {
"node": "./build/vite.js"
},
-6
View File
@@ -385,8 +385,6 @@
"src/layers/vessels/index.js",
"src/layers/vessels/ingestion.js",
"src/layers/vessels/lifecycle.js",
"src/layers/vessels/photos.js",
"src/layers/vessels/photos.test.mjs",
"src/layers/vessels/policy.js",
"src/layers/vessels/queries.js",
"src/layers/vessels/rendering.js",
@@ -1337,13 +1335,9 @@
"src/app/sourceComposition.js",
"src/app/sourceComposition.test.mjs",
"src/data/dataCredits.js",
"src/data/openVesselPhoto.js",
"src/data/openVesselPhoto.test.mjs",
"src/data/layerState.js",
"src/data/layerState.test.mjs",
"src/data/spriteOrder.js",
"src/i18n/page.js",
"src/i18n/page.test.mjs",
"src/i18n/locales/ar.json",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
+13 -36
View File
@@ -11,7 +11,8 @@
"src/data/localGeojsonLod.js",
"src/data/inputOwnership.js",
"src/data/infrastructureOverlayEntry.js",
"src/sources/infrastructureData.js"
"src/sources/infrastructureData.js",
"src/services/terrainSurface.js"
],
"external": ["cesium"]
},
@@ -175,28 +176,6 @@
],
"external": ["vue", "pinia", "vue-i18n"]
},
"i18n": {
"exports": ["./i18n"],
"modules": [
"src/i18n/page.js",
"src/i18n/index.js",
"src/i18n/locale.js",
"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/sv.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json"
],
"external": []
},
"time": {
"exports": ["./time"],
"modules": ["src/time/worldClock.js"],
@@ -1496,8 +1475,7 @@
"src/sources/live/index.js",
"src/sources/live/standalone.js",
"src/sources/live/vessels.js",
"src/data/planespottersPhoto.js",
"src/data/openVesselPhoto.js"
"src/data/planespottersPhoto.js"
],
"external": []
},
@@ -1719,9 +1697,7 @@
"src/i18n/locales/sv.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/layers/vessels/photos.js",
"src/data/openVesselPhoto.js"
"src/i18n/locales/zh-Hant.json"
],
"external": ["cesium"]
},
@@ -2086,6 +2062,7 @@
"src/layers/bikeshare/state.js",
"src/layers/bikeshare/testing.js",
"src/layers/bikeshare/viewport.js",
"src/services/terrainSurface.js",
"src/data/inputOwnership.js",
"src/data/cameraSensitivity.js",
"src/i18n/index.js",
@@ -3033,6 +3010,7 @@
"src/search/placeSearch.js",
"src/services/application.js",
"src/services/groundFloor.js",
"src/services/terrainSurface.js",
"src/services/groundSnap.js",
"src/services/meshFloorSampler.js",
"src/services/requests.js",
@@ -3782,9 +3760,7 @@
"src/data/ucsSatellites.js",
"src/data/imagingSensors.js",
"src/data/planespottersPhoto.js",
"src/ui/stores/dataLibrary.js",
"src/data/openVesselPhoto.js",
"src/layers/vessels/photos.js"
"src/ui/stores/dataLibrary.js"
],
"external": [
"hls.js",
@@ -4204,6 +4180,7 @@
"src/scopeMaskPolicy.js",
"src/scopeMask.js",
"src/services/groundFloor.js",
"src/services/terrainSurface.js",
"src/data/routeSteps.js",
"src/layers/directions/index.js",
"src/data/pointerOwnerNames.js",
@@ -4494,9 +4471,7 @@
"src/layers/satellites/sensors.js",
"src/data/ucsSatellites.js",
"src/data/imagingSensors.js",
"src/data/planespottersPhoto.js",
"src/layers/vessels/photos.js",
"src/data/openVesselPhoto.js"
"src/data/planespottersPhoto.js"
],
"external": [
"hls.js",
@@ -4550,6 +4525,7 @@
"src/search/placeSearch.js",
"src/services/application.js",
"src/services/groundFloor.js",
"src/services/terrainSurface.js",
"src/services/groundSnap.js",
"src/services/meshFloorSampler.js",
"src/services/requests.js",
@@ -5214,6 +5190,7 @@
"src/renderGovernor.js",
"src/services/application.js",
"src/services/groundFloor.js",
"src/services/terrainSurface.js",
"src/services/meshFloorSampler.js",
"src/services/requests.js",
"src/services/terrainHeights.js",
@@ -5258,6 +5235,7 @@
"src/data/transitPresetStyle.js",
"src/layers/transit/controls.js",
"src/layers/transit/height.js",
"src/services/terrainSurface.js",
"src/layers/transit/index.js",
"src/layers/transit/ingestion.js",
"src/layers/transit/lifecycle.js",
@@ -5748,8 +5726,7 @@
"src/layers/satellites/sensors.js",
"src/data/ucsSatellites.js",
"src/data/imagingSensors.js",
"src/data/planespottersPhoto.js",
"src/data/openVesselPhoto.js"
"src/data/planespottersPhoto.js"
],
"external": ["@mapbox/vector-tile", "pbf"]
},
+5 -2
View File
@@ -168,7 +168,7 @@ export function createApplicationCatalog({
createApplicationCctv({ surface, source: sources.cctv }),
createApplicationRadio({ surface, source: sources.radio }),
createApplicationTransit({ surface, source: sources.transit }),
createApplicationBikeshare({ source: sources.bikeshare }),
createApplicationBikeshare({ surface, source: sources.bikeshare }),
createApplicationDirections({ source: sources.directions }),
createApplicationRecentImagery({ source: sources['recent-imagery'] }),
createApplicationOsmPlaces(),
@@ -198,7 +198,10 @@ export function createApplicationCatalog({
}),
createApplicationCyclones({ feed: sources.cyclones }),
...createOsmOverlayLayers(),
...createInfrastructureLayers(localGeoJsonServices),
...createInfrastructureLayers({
...localGeoJsonServices,
terrainSurface: surface.groundFloor.terrainSurface,
}),
createApplicationCables({ source: sources.cables }),
createApplicationRail({ surface, source: sources.rail }),
createApplicationFirms({
+1 -2
View File
@@ -9,8 +9,7 @@ import { t } from '../i18n/index.js';
* Construct the existing controls and camera presentation. `initialShareCode`
* opens a share code (the `<code>` of `/share/<code>`) at start-up, for a host
* whose address does not carry one. `localePicker: false` removes the language
* picker, for a host that offers its own language choice (`switchLocale` from
* the `./i18n` entry).
* picker, for a host that does not initialize Core's locale.
*/
export function createApplicationControls({
scene: { viewer, mapStackController, operations },
+9 -2
View File
@@ -5,9 +5,16 @@ import * as picking from '../../data/pickRegistry.js';
import * as overlays from '../../overlays/worldOverlay.js';
/** Construct one layer using the application scene owners and a supplied source. */
export function createApplicationBikeshare({ source }) {
export function createApplicationBikeshare({ source, surface }) {
return createBikeshareLayer({
source,
services: { render, sprites, picking, overlays },
services: {
render,
sprites,
picking,
overlays,
// Station heights come from the terrain surface every layer shares.
ground: { terrainSurface: surface?.groundFloor?.terrainSurface },
},
});
}
-1
View File
@@ -29,7 +29,6 @@ export const writeCalibrationStoreV2 = layer.writeCalibrationStoreV2;
export const deriveCalBadge = layer.deriveCalBadge;
export const computeFrustumGeometry = layer.computeFrustumGeometry;
export const activationProbeClampRange = layer.activationProbeClampRange;
export const flatGroundAxisDistance = layer.flatGroundAxisDistance;
export const frameSignatureFromPixels = layer.frameSignatureFromPixels;
export const _createCctvProjectionPlaneForTest =
layer._createCctvProjectionPlaneForTest;
-12
View File
@@ -35,7 +35,6 @@ import cctvLayer, {
_pushAmbientCardEntriesForTest,
_setCctvOverlayHostForTest,
activationProbeClampRange,
flatGroundAxisDistance,
bindCctvWorldClickGesture,
clearProbeClampOnDeactivation,
computeFrustumGeometry,
@@ -656,17 +655,6 @@ test('activation obstruction clamp uses the field-derived 12 m floor', () => {
assert.equal(activationProbeClampRange(210, 250), null);
});
test('on a globe map the axis ends on flat ground at the mount ground', () => {
// A 24 m mount pitched 30° down meets the ground 48 m along its axis.
assert.ok(Math.abs(flatGroundAxisDistance(24, -30) - 48) < 1e-9);
// The default 700 m range then clamps just short of that, not 700 m out.
const clamp = activationProbeClampRange(700, flatGroundAxisDistance(24, -30));
assert.ok(Math.abs(clamp - 44) < 1e-9);
assert.equal(flatGroundAxisDistance(24, 0), null);
assert.equal(flatGroundAxisDistance(24, 10), null);
assert.equal(flatGroundAxisDistance(0, -30), null);
});
test('deactivation clears the probe clamp and restores nominal frustum geometry', () => {
const record = {
camera: { ...UNCLAMPED_CAMERA },
@@ -1,129 +0,0 @@
// An explicit pick of the camera that a settled move just re-opened must end
// the opening pick. That pick goes through setActiveCamera's UNCHANGED path
// (same camera, already activated); if that path keeps the provisional flag,
// the next settled move (a pan, or the landing of NEAREST's own flight)
// switches the camera away from the one the user chose.
import assert from 'node:assert/strict';
import test from 'node:test';
import * as Cesium from 'cesium';
import cctvLayer, {
_setCctvCoverageStateForTest,
_setCctvOverlayHostForTest,
reopenNearestCamera,
} from './cctv.js';
const BASE = {
headingDeg: 41,
pitchDeg: -5,
fovDeg: 56,
rangeM: 210,
mountHeightM: 100,
groundElevationM: 0,
city: 'London',
};
function record(id, lat, lon) {
return {
camera: { ...BASE, id, name: `Camera ${id}`, lat, lon },
coverageEntities: [],
projection: null,
activationDone: true,
groundResolved: {},
groundSamples: {},
groundPrior: 0,
frustumGeometry: null,
frustumPositions: null,
probeClampRangeM: null,
position: Cesium.Cartesian3.fromDegrees(lon, lat, 100),
};
}
function viewerAt(lat, lon) {
return {
entities: new Cesium.EntityCollection(),
isDestroyed: () => false,
trackedEntity: null,
camera: {
positionWC: Cesium.Cartesian3.fromDegrees(lon, lat, 300),
positionCartographic: Cesium.Cartographic.fromDegrees(lon, lat, 300),
heading: 0.4,
pitch: -0.7,
roll: 0,
transform: Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY),
flyToBoundingSphere() {},
},
scene: {
canvas: { clientWidth: 1200, clientHeight: 800 },
cartesianToCanvasCoordinates: () => undefined,
primitives: { add: (p) => p, remove: () => true },
requestRender() {},
},
};
}
function moveViewer(viewer, lat, lon) {
viewer.camera.positionCartographic = Cesium.Cartographic.fromDegrees(
lon,
lat,
300,
);
}
function withReopenedLayer(fn) {
_setCctvOverlayHostForTest({
setEntries() {},
setVisible() {},
clearSource() {},
});
// Activation builds the projection canvas and frame image.
const originalDocument = globalThis.document;
const originalImage = globalThis.Image;
globalThis.document = { createElement: () => ({ getContext: () => null }) };
globalThis.Image = class {};
try {
const trafalgar = record('a-trafalgar', 51.508, -0.128);
const cambridge = record('b-cambridge', 51.513, -0.129);
// enable() assigned the opening camera without activating it.
trafalgar.activationDone = false;
const viewer = viewerAt(51.5081, -0.1281);
_setCctvCoverageStateForTest({
viewer,
records: [trafalgar, cambridge],
activeCameraId: 'a-trafalgar',
enabled: true,
openingCameraProvisional: true,
});
// A settled move next to Cambridge Circus re-opens there, still as the
// opening pick.
moveViewer(viewer, 51.5131, -0.1291);
assert.equal(reopenNearestCamera(), true);
assert.equal(cctvLayer.getUIState().activeCameraId, 'b-cambridge');
fn(viewer);
} finally {
globalThis.document = originalDocument;
globalThis.Image = originalImage;
_setCctvOverlayHostForTest();
_setCctvCoverageStateForTest({ enabled: false });
}
}
for (const [name, pick] of [
['NEAREST (focusNearest)', () => cctvLayer.focusNearest({ focus: false })],
['selectCamera', () => cctvLayer.selectCamera('b-cambridge')],
[
'setParams({ selectedCameraId })',
() => cctvLayer.setParams({ selectedCameraId: 'b-cambridge' }),
],
]) {
test(`an explicit ${name} pick of the re-opened camera ends the opening pick`, () => {
withReopenedLayer((viewer) => {
pick();
assert.equal(cctvLayer.getUIState().activeCameraId, 'b-cambridge');
// A later settled move (a pan, or the landing of the pick's own flight)
// leaves the explicit choice alone.
moveViewer(viewer, 51.5081, -0.1281);
assert.equal(reopenNearestCamera(), false);
assert.equal(cctvLayer.getUIState().activeCameraId, 'b-cambridge');
});
});
}
-7
View File
@@ -141,13 +141,6 @@ export const DATA_CREDITS = [
'<a href="https://www.planespotters.net" target="_blank" rel="noopener">Planespotters.net</a> ' +
'· each photo is © its photographer, credited beside it',
},
{
key: 'openvesselphotos',
html:
'Vessel photos: ' +
'<a href="https://openvesselphotos.org" target="_blank" rel="noopener">Open Vessel Photos</a> ' +
"· each photo keeps its author's open licence, credited beside it",
},
{
key: 'aisstream',
html:
-7
View File
@@ -51,13 +51,6 @@ test('Planespotters is credited with a plain followable link', () => {
assert.doesNotMatch(credit.html, /nofollow/);
});
test('Open Vessel Photos is credited with a plain followable link', () => {
const credit = DATA_CREDITS.find((entry) => entry.key === 'openvesselphotos');
assert.ok(credit, 'the selected vessel photos come from Open Vessel Photos');
assert.match(credit.html, /href="https:\/\/openvesselphotos\.org"/);
assert.doesNotMatch(credit.html, /nofollow/);
});
test('the UCS Satellite Database is acknowledged with its version and webpage', () => {
// UCS database manual, section 3: references name the version (the Excel
// file's name) and link www.ucsusa.org/satellite_database.
+1
View File
@@ -196,6 +196,7 @@ test('consumer build includes only infrastructure code and resolves assets under
'infrastructureOverlayEntry.js',
'localGeojsonCore.js',
'localGeojsonLod.js',
'terrainSurface.js',
]);
assert.deepEqual(
entry.imports,
+185 -5
View File
@@ -23,9 +23,10 @@ import {
} from '../renderGovernor.js';
/** The catalog's services, with the overlay host each test supplies. */
const createLocalGeoJsonLayer = (options) =>
const createLocalGeoJsonLayer = (options, services = {}) =>
createLayer(options, {
...localGeoJsonServices,
...services,
overlayHost: options.overlayHost ?? localGeoJsonServices.overlayHost,
});
@@ -58,10 +59,15 @@ class MockLayerEvent {
* @param {boolean} [options.sampleHeightSupported] Scene height-sampling capability.
* @param {Function} [options.sampleHeight] Initial scene.sampleHeight behavior
* (default: throws like a scene whose tiles are not sampleable yet).
* @param {number[][]} [options.points] `[lon, lat]` sites to load
* instead of the single dam polygon.
* @param {object} [options.services] Extra services for the layer.
*/
async function createRealLocalLayerHarness({
sampleHeightSupported = false,
sampleHeight = () => { throw new Error('tiles not sampleable yet'); },
points = null,
services = {},
} = {}) {
const originalFetch = globalThis.fetch;
const originalWindow = globalThis.window;
@@ -72,7 +78,22 @@ async function createRealLocalLayerHarness({
globalThis.fetch = async () => ({
ok: true,
status: 200,
text: async () => JSON.stringify({
text: async () => points ? points.map(([lon, lat], index) => JSON.stringify({
type: 'Feature',
id: `site-${index}`,
properties: { name: `Site ${index}` },
// A small outline: the layer stems its centre. (Cesium draws a point
// feature as a canvas pin, which Node cannot make.)
geometry: {
type: 'Polygon',
coordinates: [[
[lon - 0.001, lat - 0.001],
[lon + 0.001, lat - 0.001],
[lon, lat + 0.001],
[lon - 0.001, lat - 0.001],
]],
},
})).join('\n') : JSON.stringify({
type: 'Feature',
id: 'real-dam',
properties: { name: 'Runtime Dam', tags: { associated_river: 'Test River' } },
@@ -149,7 +170,7 @@ async function createRealLocalLayerHarness({
setInputAction() {},
destroy() {},
}),
});
}, services);
try {
await layer.enable(viewer);
} finally {
@@ -903,12 +924,13 @@ test('settled terrain refinement lifts an already sampled stem without another G
env.preRender.raise();
assert.ok(Math.abs(baseHeightM(env) - 117) < 0.01, 'wait for the refined mesh to settle');
globe.tilesLoaded = true;
clock.advance(500);
// The terrain surface reuses a height for 2 s before reading the mesh again.
clock.advance(2_100);
env.preRender.raise();
assert.ok(Math.abs(baseHeightM(env) - 120) < 0.01, 'a parked marker must follow the refined floor');
terrain = 110;
clock.advance(500);
clock.advance(2_100);
env.preRender.raise();
assert.ok(Math.abs(baseHeightM(env) - 120) < 0.01, 'a lower floor must not flatten existing geometry');
terrain = 140;
@@ -1493,3 +1515,161 @@ test('globe-LOD re-selects during continuous motion, without ever seeing a moveE
assert.deepEqual(env.layer.getLodDiagnostics(), parked, 'a parked camera never re-selects');
});
/** A loaded terrain globe that counts its ray casts. */
function countingTerrain(height) {
const globe = {
show: true,
tilesLoaded: true,
reads: 0,
getHeight: () => { globe.reads += 1; return height; },
};
return globe;
}
/** Height of every stem base, in entity order. */
function allBaseHeightsM(env) {
return env.dataSources[0].entities.values.map(
(entity) => Cesium.Cartographic.fromCartesian(entity.__localBaseCartesian).height,
);
}
const SAMPLE_SITES = [
[-97.70, 30.20], [-97.69, 30.20], [-97.70, 30.21], [-97.69, 30.21], [-97.695, 30.205],
];
test('repeated walks over a grounded stem reuse its terrain height instead of re-casting it', async (t) => {
const env = await createRealLocalLayerHarness({
sampleHeightSupported: true, sampleHeight: () => 117,
});
const clock = installFakeClock(t);
t.after(() => { env.layer.destroy(env.viewer); env.cleanup(); });
const globe = countingTerrain(117);
env.viewer.scene.globe = globe;
setCameraAltitude(env, 20_000);
for (let walk = 0; walk < 40; walk += 1) {
env.preRender.raise();
clock.advance(500);
}
assert.ok(Math.abs(baseHeightM(env) - 117) < 0.01);
// 20 s parked: a few reads while the surface settles, then none.
assert.ok(globe.reads <= 8, `${globe.reads} terrain ray casts over 40 parked walks`);
const settled = globe.reads;
for (let walk = 0; walk < 20; walk += 1) {
env.preRender.raise();
clock.advance(500);
}
assert.equal(globe.reads, settled, 'a parked camera over settled terrain casts no more rays');
});
test('repeated walks past an unseen base do not re-cast its terrain height', async (t) => {
const env = await createRealLocalLayerHarness({
sampleHeightSupported: true, sampleHeight: () => 117,
});
const clock = installFakeClock(t);
t.after(() => { env.layer.destroy(env.viewer); env.cleanup(); });
const globe = countingTerrain(117);
env.viewer.scene.globe = globe;
setCameraAltitude(env, 20_000);
env.setBaseInView(false);
for (let walk = 0; walk < 40; walk += 1) {
env.preRender.raise();
clock.advance(500);
}
assert.equal(env.sampleCalls.count, 0);
assert.ok(globe.reads <= 8, `${globe.reads} terrain ray casts over 40 walks past an unseen base`);
});
test('terrain heights come from the shared surface the host supplies, in degrees', async (t) => {
const asked = [];
const terrainSurface = {
height: (viewer, lat, lon, need) => { asked.push({ viewer, lat, lon, need }); return 130; },
};
const env = await createRealLocalLayerHarness({
sampleHeightSupported: true, sampleHeight: () => 117,
points: [[-97.695, 30.205]],
services: { terrainSurface },
});
const clock = installFakeClock(t);
t.after(() => { env.layer.destroy(env.viewer); env.cleanup(); });
const globe = countingTerrain(117);
env.viewer.scene.globe = globe;
setCameraAltitude(env, 20_000);
env.preRender.raise();
assert.ok(Math.abs(baseHeightM(env) - 117) < 0.01, 'the sample is floored by the globe itself');
clock.advance(500);
env.preRender.raise();
assert.ok(asked.length >= 2);
for (const call of asked) {
assert.equal(call.viewer, env.viewer);
assert.ok(Math.abs(call.lat - 30.205) < 0.002 && Math.abs(call.lon + 97.695) < 0.002,
`asked at ${call.lat}, ${call.lon}`);
}
assert.ok(Math.abs(baseHeightM(env) - 130) < 0.01, 'the refined floor comes from the shared surface');
assert.equal(globe.reads, 1, 'only the floor under a fresh sample is read from the globe directly');
});
test('a walk of expensive ground samples stops at its budget and later frames finish the rest', async (t) => {
const heights = new Map();
let clock = null;
const env = await createRealLocalLayerHarness({
sampleHeightSupported: true,
points: SAMPLE_SITES,
sampleHeight: (carto) => {
clock.advance(15); // one GPU depth render
const height = 100 + heights.size;
heights.set(`${carto.longitude},${carto.latitude}`, height);
return height;
},
});
_resetRenderGovernorForTest();
const governorScene = { renders: 0, requestRender() { this.renders += 1; } };
installRenderGovernor({ scene: governorScene });
governorScene.renders = 0; // discard the governor's own install settling frame
clock = installFakeClock(t);
t.after(() => {
env.layer.destroy(env.viewer);
env.cleanup();
_resetRenderGovernorForTest();
});
setCameraAltitude(env, 20_000);
env.preRender.raise();
assert.equal(env.sampleCalls.count, 1, 'one 15 ms sample spends the walk budget');
assert.equal(governorScene.renders, 1, 'the walk asks for the frame that continues it');
// The following frames arrive well inside the 450 ms walk interval.
for (let frame = 2; frame <= SAMPLE_SITES.length; frame += 1) {
clock.advance(16);
env.preRender.raise();
assert.equal(env.sampleCalls.count, frame, 'each frame takes up where the last stopped');
}
assert.equal(governorScene.renders, SAMPLE_SITES.length - 1, 'no frame is requested once every site is sampled');
clock.advance(16);
env.preRender.raise();
assert.equal(env.sampleCalls.count, SAMPLE_SITES.length, 'nothing is sampled twice');
const expected = env.dataSources[0].entities.values.map((entity) => {
const carto = entity.__localBaseCarto;
return heights.get(`${carto.longitude},${carto.latitude}`);
});
assert.deepEqual(allBaseHeightsM(env).map((h) => Math.round(h * 100) / 100), expected);
for (const entity of env.dataSources[0].entities.values) {
const stem = entity.polyline.positions.getValue();
assert.ok(Cesium.Cartesian3.equals(stem[0], entity.__localBaseCartesian), 'each stem stands on its sampled base');
}
});
test('cheap ground samples are all taken on the first walk, at the same heights as paced ones', async (t) => {
const env = await createRealLocalLayerHarness({
sampleHeightSupported: true,
points: SAMPLE_SITES,
sampleHeight: () => 117,
});
installFakeClock(t);
t.after(() => { env.layer.destroy(env.viewer); env.cleanup(); });
setCameraAltitude(env, 20_000);
env.preRender.raise();
assert.equal(env.sampleCalls.count, SAMPLE_SITES.length);
for (const height of allBaseHeightsM(env)) assert.ok(Math.abs(height - 117) < 0.01);
});
+134 -15
View File
@@ -11,6 +11,10 @@ import {
applyInfraEvictionGrace,
shouldRecomputeInfraLod,
} from './localGeojsonLod.js';
import {
createTerrainSurface,
TERRAIN_NEED,
} from '../services/terrainSurface.js';
const DEFAULT_LABEL_MAX = 900;
const DEFAULT_LABEL_GRID_PX = 132;
@@ -38,6 +42,15 @@ const GROUND_SAMPLE_MAX_ABS_HEIGHT_M = 9000;
* 30 × 2 s ≈ 60 s, far longer than a tile stream-in.
*/
export const GROUND_SAMPLE_MAX_ARMED_RETRIES = 30;
/**
* Milliseconds of scene.sampleHeight one frame may spend. Each call is an
* offscreen depth render (about 15 ms measured), and a first walk near a dense
* cluster wants one per site in view: 168 of them held the main thread for
* 2.6 s. Past the budget the remaining sites queue, and each following frame
* takes as many as fit, so the sites reach the same heights a frame or a few
* apart instead of all inside one blocked frame.
*/
export const GROUND_SAMPLE_FRAME_BUDGET_MS = 8;
/** Ignore sub-metre camera-derived stem-tip noise at camera settle. */
export const LOCAL_STEM_TIP_EPSILON_M = 0.5;
const LOCAL_STEM_TIP_EPSILON_SQ = LOCAL_STEM_TIP_EPSILON_M ** 2;
@@ -336,8 +349,20 @@ export function createLocalGeoJsonLayer(
governorRequestRender,
showOsmCredit,
hideOsmCredit,
terrainSurface,
},
) {
/**
* What ground sampling shares across a layer's records: the terrain
* surface (the application's, so every layer reads a point once and spends
* one budget, or this layer's own), the sample time spent this frame, and
* the records the frame budget left for a later frame.
*/
const _ground = {
terrain: terrainSurface || createTerrainSurface(),
sampleMs: 0,
queued: [],
};
let _dataSource = null;
let _enabled = false;
let _clickHandler = null;
@@ -444,6 +469,42 @@ export function createLocalGeoJsonLayer(
clearTimeout(_groundRetryTimer);
_groundRetryTimer = null;
}
/**
* Sample the sites an earlier frame's budget left over, within this
* frame's. They were never attempted, so the 2 s retry spacing does not
* apply to them.
* @param {Cesium.Viewer} viewer
* @param {number} now
* @returns {void}
*/
function sampleQueuedGround(viewer, now) {
const queued = _ground.queued;
if (queued.length === 0) return;
_ground.queued = [];
for (const record of queued) {
record.groundSampleQueued = false;
// The camera may have left the site since it queued.
if (_lodComputed && !_activeLodIds.has(record.id)) continue;
const distance = Cesium.Cartesian3.distance(
viewer.camera.positionWC,
record.base,
);
if (distance >= GROUND_SAMPLE_MAX_DISTANCE_M) continue;
// Past the budget this puts the record back in the queue.
if (sampleLocalGroundHeight(viewer, record, now, _ground)) {
updateLocalStemGeometry(viewer, record, now, _ground, distance);
_groundRetryArms = 0;
}
}
}
/** Ask for the frame that samples what this one's budget left over. */
function requestQueuedGroundFrame() {
if (_ground.queued.length > 0)
governorRequestRender(`local-ground-sample:${id}`);
}
const _overlayPublisher = createLocalInfrastructureOverlayPublisher({
sourceId: id,
host: overlayHost,
@@ -472,6 +533,7 @@ export function createLocalGeoJsonLayer(
const source = _dataSource;
_dataSource = null;
_stemRecords = [];
_ground.queued = [];
_stemGeometryDirty = true;
_lastVisibilityUpdate = Number.NEGATIVE_INFINITY;
removeEntityContextsForLayer(id);
@@ -744,6 +806,8 @@ export function createLocalGeoJsonLayer(
// Store references for bounded stem scaling and native picking.
feature.__localBaseCarto = carto;
feature.__localBaseCartesian = base;
const latDeg = Cesium.Math.toDegrees(carto.latitude);
const lonDeg = Cesium.Math.toDegrees(carto.longitude);
const context = registerEntityContext(feature, {
id: `${id}:${recordId}`,
layerId: id,
@@ -795,7 +859,11 @@ export function createLocalGeoJsonLayer(
stemPositionBuffers,
stemPositionBufferIndex: 0,
groundHeight,
// Degrees, as the terrain surface takes them.
latDeg,
lonDeg,
groundSampled: false,
groundSampleQueued: false,
lastGroundSampleMs: 0,
priority,
entry: labels
@@ -916,7 +984,13 @@ export function createLocalGeoJsonLayer(
_preRenderRemover = viewer.scene.preRender.addEventListener(() => {
if (!_enabled || !_dataSource) return;
const now = performance.now();
if (now - _lastVisibilityUpdate < VISIBILITY_UPDATE_MS) return;
// One sample budget per frame, shared by the queue and the walk.
_ground.sampleMs = 0;
sampleQueuedGround(viewer, now);
if (now - _lastVisibilityUpdate < VISIBILITY_UPDATE_MS) {
requestQueuedGroundFrame();
return;
}
_lastVisibilityUpdate = now;
const cameraPos = viewer.camera.positionWC;
@@ -967,6 +1041,9 @@ export function createLocalGeoJsonLayer(
_lastGroundSampleCapability = canSampleGround;
let groundRetryPending = false;
let groundSampleProgress = false;
// This walk asks for no height while tiles load, so it shows the
// surface the load itself; see terrainSurface.js observe().
_ground.terrain.observe?.(viewer);
// --- Globe-LOD: recompute the active-stem set on camera moves. ---
// `_stemGeometryDirty` is set by moveEnd, the first enable, and the
@@ -1028,9 +1105,10 @@ export function createLocalGeoJsonLayer(
const terrainFloorChanged = refreshLocalTerrainFloor(
viewer,
record,
_ground,
);
if (refreshStemGeometry || terrainFloorChanged) {
updateLocalStemGeometry(viewer, record, now);
updateLocalStemGeometry(viewer, record, now, _ground);
} else if (
canSampleGround &&
!record.groundSampled &&
@@ -1044,9 +1122,9 @@ export function createLocalGeoJsonLayer(
);
if (
distance < GROUND_SAMPLE_MAX_DISTANCE_M &&
sampleLocalGroundHeight(viewer, record, now)
sampleLocalGroundHeight(viewer, record, now, _ground)
) {
updateLocalStemGeometry(viewer, record, now, distance);
updateLocalStemGeometry(viewer, record, now, _ground, distance);
}
}
if (!wasGroundSampled && record.groundSampled)
@@ -1058,16 +1136,19 @@ export function createLocalGeoJsonLayer(
// the user happens to move. Schedule the frame the retry needs.
// Gated on a sampleable scene and in-range, in-view records only,
// so a far camera (or a keyless scene) stays fully idle; the
// distance is only computed for still-unsampled stems.
// distance is only computed for still-unsampled stems. A record
// the frame budget queued is sampled by the frame the queue asks
// for, with no timer.
if (
canSampleGround &&
!record.groundSampled &&
!record.groundSampleQueued &&
!groundRetryPending &&
Cesium.Cartesian3.distance(
viewer.camera.positionWC,
record.base,
) < GROUND_SAMPLE_MAX_DISTANCE_M &&
groundSampleInView(viewer, record)
groundSampleInView(viewer, record, _ground)
) {
groundRetryPending = true;
}
@@ -1078,6 +1159,7 @@ export function createLocalGeoJsonLayer(
// so the records still waiting get their own bounded run of retries.
if (groundSampleProgress) _groundRetryArms = 0;
if (groundRetryPending) scheduleGroundRetryRender(viewer);
requestQueuedGroundFrame();
const canvas = viewer.scene.canvas;
const cohort = selectLocalInfrastructureOverlayCohort(
@@ -1268,7 +1350,7 @@ function insertLocalCellContender(contenders, record) {
contenders.length = LOCAL_OVERLAY_CELL_SURPLUS;
}
function refreshLocalTerrainFloor(viewer, record) {
function refreshLocalTerrainFloor(viewer, record, ground) {
const globe = viewer.scene.globe;
if (!record.groundSampled || !globe?.show || globe.tilesLoaded === false)
return false;
@@ -1280,7 +1362,15 @@ function refreshLocalTerrainFloor(viewer, record) {
// Camera motion can refine terrain after the first successful depth sample.
// Reuse the loaded mesh on existing bounded walks; never arm a timer or do
// another GPU readback just to maintain this floor. Keep roof elevations.
const height = globe.getHeight?.(record.carto);
// The surface keeps the height per point and casts the ray again only after
// the camera moves or tiles load. MARKER: the answer is this point's own
// height, never a neighbour's, since a height taken here is kept for good.
const height = ground.terrain.height(
viewer,
record.latDeg,
record.lonDeg,
TERRAIN_NEED.MARKER,
);
if (
!Number.isFinite(height) ||
Math.abs(height) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M ||
@@ -1323,11 +1413,20 @@ function groundSamplePoint(record, height, result) {
* sample can accept, so high or below-ellipsoid ground in view is never
* mistaken for an unseen base. Unseen ground is sampled on the first walk that
* sees it.
*
* The terrain height comes from the shared surface, which keeps it per point:
* this runs for every unsampled site in range on every walk, and a ray cast
* each time was 17,733 casts and 1.3 s over one zoom. The view test needs the
* height only roughly, so a height the surface has not read yet, or tiles
* still loading, count as unknown ground.
*/
function groundSampleInView(viewer, record) {
function groundSampleInView(viewer, record, ground) {
const camera = viewer.camera;
const globe = viewer.scene.globe;
const terrain = globe?.show ? globe.getHeight?.(record.carto) : undefined;
const terrain =
globe?.show && globe.tilesLoaded !== false
? ground.terrain.height(viewer, record.latDeg, record.lonDeg)
: undefined;
const known =
Number.isFinite(terrain) &&
Math.abs(terrain) <= GROUND_SAMPLE_MAX_ABS_HEIGHT_M;
@@ -1368,18 +1467,30 @@ function groundSampleInView(viewer, record) {
);
}
function sampleLocalGroundHeight(viewer, record, now) {
function sampleLocalGroundHeight(viewer, record, now, ground) {
if (record.groundSampled || !viewer.scene.sampleHeightSupported) return false;
if (now - record.lastGroundSampleMs < GROUND_SAMPLE_RETRY_MS) return false;
if (!groundSampleInView(viewer, record)) return false;
// This frame's samples have used its budget: leave the record for the next
// frame, unattempted, so the retry spacing does not hold it back.
if (ground.sampleMs >= GROUND_SAMPLE_FRAME_BUDGET_MS) {
if (!record.groundSampleQueued) {
record.groundSampleQueued = true;
ground.queued.push(record);
}
return false;
}
if (!groundSampleInView(viewer, record, ground)) return false;
record.lastGroundSampleMs = now;
const globe = viewer.scene.globe;
if (globe?.show && globe.tilesLoaded === false) return false;
let sampled;
const startedAt = performance.now();
try {
sampled = viewer.scene.sampleHeight(record.carto, [record.entity]);
} catch {
return false; // tiles not ready; retry on a later bounded update
} finally {
ground.sampleMs += performance.now() - startedAt;
}
if (
!Number.isFinite(sampled) ||
@@ -1389,7 +1500,9 @@ function sampleLocalGroundHeight(viewer, record, now) {
// A coarse scene-depth sample can lie below the terrain already loaded by
// the visible globe. Keep that terrain as a floor while allowing roofs and
// valid below-sea-level elevations. Photoreal scenes hide the globe, so its
// inactive terrain must not constrain their geometry.
// inactive terrain must not constrain their geometry. Read from the globe
// itself, once per site: the floor under a sample must be the mesh that is
// loaded now, not a height kept from a moment before.
const terrainHeight = globe?.show
? globe.getHeight?.(record.carto)
: undefined;
@@ -1416,12 +1529,18 @@ function setLocalGroundHeight(record, height) {
record.entity.__localBaseCartesian = record.base;
}
function updateLocalStemGeometry(viewer, record, now, knownDistance = null) {
function updateLocalStemGeometry(
viewer,
record,
now,
ground,
knownDistance = null,
) {
const distance = Number.isFinite(knownDistance)
? knownDistance
: Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base);
if (distance < GROUND_SAMPLE_MAX_DISTANCE_M)
sampleLocalGroundHeight(viewer, record, now);
sampleLocalGroundHeight(viewer, record, now, ground);
// Keep the intended screen-size scaling in close-up views too. A 5 km
// minimum made a marker hundreds of metres tall beside a nearby building.
const effectiveDistance = Math.max(distance, 1);
-135
View File
@@ -1,135 +0,0 @@
/**
* @module openVesselPhoto
* @description Reads an Open Vessel Photos answer into the one photo every
* surface shows. Open Vessel Photos (https://openvesselphotos.org) serves a
* free, keyless JSON and image API by MMSI. Every photo keeps the open licence
* its author chose (CC BY, CC BY-SA, CC0 or public domain), so it is shown
* with its credit and licence and links back to the vessel's page. A photo
* whose image is not on Open Vessel Photos' own host, or whose answer names
* no licence, is "no photo": nothing arbitrary is ever shown or linked.
*
* Accepts the upstream answer (`GET https://openvesselphotos.org/v1/mmsi/{mmsi}`,
* `{mmsi, photos: [...]}`) and an already-read photo
* (`{found, src, width, height, photographer, licence, link}`).
* Pure: no fetch, no DOM.
*/
/** The only host a photo may come from or link to. */
const HOST = 'openvesselphotos.org';
/** The API's per-vessel answer. */
const API = `https://${HOST}/v1/mmsi/`;
/** The image width asked for: sharp in the panel on a 2× screen, ~60 KB. */
const IMAGE_SIZE = '800';
const NO_PHOTO = Object.freeze({ found: false });
/** A vessel's MMSI: nine digits. */
export const MMSI = /^\d{9}$/;
/** The API answer for one MMSI. */
export function openVesselPhotosUrl(mmsi) {
return API + mmsi;
}
/** The vessel's page on Open Vessel Photos. */
function vesselPage(mmsi) {
return `https://${HOST}/vessels/mmsi/${mmsi}`;
}
/** The parsed `https:` URL on the Open Vessel Photos host, or null. */
function ownUrl(value) {
if (typeof value !== 'string' || !value) return null;
let url;
try {
url = new URL(value);
} catch {
return null;
}
return url.protocol === 'https:' &&
!url.username &&
!url.password &&
!url.port &&
url.hostname === HOST
? url
: null;
}
function dimension(value) {
const number = Number(value);
return value != null && Number.isFinite(number) && number > 0
? Math.round(number)
: null;
}
function text(value) {
return typeof value === 'string' ? value.trim() : '';
}
/** One upstream photo of the vessel `mmsi`, checked. */
function checkedUpstream(photo, mmsi) {
if (!photo || typeof photo !== 'object') return null;
const image = ownUrl(photo.urls?.mmsi);
const licence = text(photo.licenceName) || text(photo.licence);
if (!image || !licence) return null;
image.search = '';
image.hash = '';
image.searchParams.set('size', IMAGE_SIZE);
// The answer gives the full image's size; the 800 px one keeps its shape.
const width = dimension(photo.width);
const height = dimension(photo.height);
const scale =
width && width > Number(IMAGE_SIZE) ? Number(IMAGE_SIZE) / width : 1;
return {
found: true,
src: image.href,
width: width ? Math.round(width * scale) : null,
height: height ? Math.round(height * scale) : null,
photographer: text(photo.credit),
licence,
link: vesselPage(mmsi),
};
}
/** An already-read photo, checked again. */
function checkedRead(photo) {
const image = ownUrl(photo.src);
const page = ownUrl(photo.link);
const licence = text(photo.licence);
if (!image || !page || !licence) return null;
return {
found: true,
src: image.href,
width: dimension(photo.width),
height: dimension(photo.height),
photographer: text(photo.photographer),
licence,
link: page.href,
};
}
/**
* Read an Open Vessel Photos answer into the first photo that passes the
* checks.
* @param {*} payload Upstream answer or already-read photo.
* @param {?string} [requestedMmsi] The MMSI asked for; an upstream answer
* naming another is no photo.
* @returns {{found: true, src: string, width: ?number, height: ?number,
* photographer: string, licence: string, link: string} | {found: false}}
*/
export function parseOpenVesselPhoto(payload, requestedMmsi = null) {
if (!payload || typeof payload !== 'object') return NO_PHOTO;
if (Array.isArray(payload.photos)) {
const mmsi = String(payload.mmsi ?? '');
if (!MMSI.test(mmsi)) return NO_PHOTO;
// An answer about another vessel is no photo of this one.
if (requestedMmsi != null && mmsi !== String(requestedMmsi))
return NO_PHOTO;
for (const photo of payload.photos) {
const checked = checkedUpstream(photo, mmsi);
if (checked) return checked;
}
return NO_PHOTO;
}
if (payload.found !== true) return NO_PHOTO;
return checkedRead(payload) || NO_PHOTO;
}
-120
View File
@@ -1,120 +0,0 @@
// src/data/openVesselPhoto.test.mjs — an Open Vessel Photos answer is read
// into one credited, licensed photo, and nothing outside Open Vessel Photos'
// own host is ever shown or linked.
import test from 'node:test';
import assert from 'node:assert/strict';
import {
openVesselPhotosUrl,
parseOpenVesselPhoto,
} from './openVesselPhoto.js';
const UPSTREAM = Object.freeze({
mmsi: '366999709',
name: '01 USCGC Sockeye WPB87337',
photos: [
{
n: 1,
urls: {
cfr: null,
mmsi: 'https://openvesselphotos.org/v1/mmsi/366999709/photos/1.jpg',
},
credit: 'Gordon Leggett',
licence: 'CC-BY-4.0',
licenceName: 'CC BY 4.0',
width: 1600,
height: 1200,
},
],
});
const PHOTO = Object.freeze({
found: true,
src: 'https://openvesselphotos.org/v1/mmsi/366999709/photos/1.jpg?size=800',
width: 800,
height: 600,
photographer: 'Gordon Leggett',
licence: 'CC BY 4.0',
link: 'https://openvesselphotos.org/vessels/mmsi/366999709',
});
const withPhoto = (overrides) => ({
...UPSTREAM,
photos: [{ ...UPSTREAM.photos[0], ...overrides }],
});
test('the upstream answer maps to the 800 px image, its credit, licence and page', () => {
assert.deepEqual(parseOpenVesselPhoto(UPSTREAM), PHOTO);
assert.equal(
openVesselPhotosUrl('366999709'),
'https://openvesselphotos.org/v1/mmsi/366999709',
);
});
test('an image narrower than 800 px keeps its own size', () => {
const photo = parseOpenVesselPhoto(withPhoto({ width: 640, height: 480 }));
assert.equal(photo.width, 640);
assert.equal(photo.height, 480);
});
test('an already-read photo passes through its checks again', () => {
assert.deepEqual(parseOpenVesselPhoto(PHOTO), PHOTO);
assert.deepEqual(parseOpenVesselPhoto({ ...PHOTO, licence: '' }), {
found: false,
});
});
test('a photo off the Open Vessel Photos host, or without a licence, is no photo', () => {
for (const overrides of [
{ urls: { mmsi: 'https://example.com/1.jpg' } },
{
urls: {
mmsi: 'http://openvesselphotos.org/v1/mmsi/366999709/photos/1.jpg',
},
},
{ urls: { mmsi: 'https://openvesselphotos.org.evil.com/1.jpg' } },
{ urls: { mmsi: 'https://[email protected]/1.jpg' } },
{ licence: '', licenceName: '' },
])
assert.deepEqual(parseOpenVesselPhoto(withPhoto(overrides)), {
found: false,
});
assert.deepEqual(
parseOpenVesselPhoto({ ...PHOTO, link: 'https://example.com/' }),
{ found: false },
);
});
test('a vessel without photos, or an answer with a bad MMSI, is no photo', () => {
assert.deepEqual(parseOpenVesselPhoto({ ...UPSTREAM, photos: [] }), {
found: false,
});
assert.deepEqual(parseOpenVesselPhoto({ ...UPSTREAM, mmsi: '12/../x' }), {
found: false,
});
assert.deepEqual(parseOpenVesselPhoto(null), { found: false });
});
test('the next photo stands in when the first fails its checks', () => {
const second = {
...UPSTREAM.photos[0],
n: 2,
urls: {
mmsi: 'https://openvesselphotos.org/v1/mmsi/366999709/photos/2.jpg',
},
};
const photo = parseOpenVesselPhoto({
...UPSTREAM,
photos: [{ ...UPSTREAM.photos[0], licenceName: '', licence: '' }, second],
});
assert.equal(
photo.src,
'https://openvesselphotos.org/v1/mmsi/366999709/photos/2.jpg?size=800',
);
});
test('an answer about another vessel than the one asked for is no photo', () => {
assert.deepEqual(parseOpenVesselPhoto(UPSTREAM, '366999709'), PHOTO);
assert.deepEqual(parseOpenVesselPhoto(UPSTREAM, '227000101'), {
found: false,
});
});
+67 -69
View File
@@ -1,5 +1,5 @@
{
"News feed unavailable": "مصدر بيانات الأخبار غير متاح",
"News feed unavailable": "موجز الأخبار غير متاح",
"Developing: {name}": "تطورات: {name}",
"{count} events today": {
"zero": "{count} حدث اليوم",
@@ -39,13 +39,13 @@
"Iran–Hormuz": "إيران–هرمز",
"Yemen–Red Sea": "اليمن–البحر الأحمر",
"Sudan": "السودان",
"Sahel": "الساحل الأفريقي",
"Sahel": "الساحل",
"Elsewhere": "أماكن أخرى",
"Strike": "ضربة",
"Alert": "إنذار",
"Air defence": "دفاع جوي",
"Ground fighting": "قتال بري",
"Casualties": "ضحايا",
"Casualties": "إصابات",
"Humanitarian": "إنساني",
"Disaster": "كارثة",
"Protest": "احتجاج",
@@ -65,20 +65,20 @@
"many": "{count} مصدرًا",
"other": "{count} مصدر"
},
"News feed not configured on this server": "مصدر بيانات الأخبار غير مهيأ على هذا الخادم",
"News feed not configured on this server": "موجز الأخبار غير مهيأ على هذا الخادم",
"Malformed news hotspots response": "استجابة بؤر الأخبار غير صالحة",
"Malformed news feed response": "استجابة مصدر بيانات الأخبار غير صالحة",
"Malformed news feed response": "استجابة موجز الأخبار غير صالحة",
"{place} (placed by model)": "{place} (حدده النموذج)",
"English headline by the model": "عنوان إنجليزي من النموذج",
"Not placed on the map": "لم يُحدَّد موقعه على الخريطة",
"Not placed on the map": "غير موضوع على الخريطة",
"Close": "إغلاق",
"Any severity": "أي درجة خطورة",
"{severity} and above": "{severity} فأعلى",
"{severity} only": "{severity} فقط",
"All categories": "كل الفئات",
"Reading the news…": "جارٍ قراءة الأخبار…",
"News, last {hours} hours": "الأخبار، الساعات الأخيرة: {hours}",
"Last {hours} h": "آخر {hours} س",
"News, last {hours} hours": "الأخبار، آخر {hours} ساعة",
"Last {hours} h": "آخر {hours} ساعة",
"Time window": "الفترة الزمنية",
"Conflicts": "النزاعات",
"Local": "محلي",
@@ -87,9 +87,9 @@
"Hotspots": "بؤر ساخنة",
"Severity": "الخطورة",
"Category": "الفئة",
"Nothing reported near here in the last {hours} h": "لم يُبلَّغ عن شيء قريب من هنا في آخر {hours} س",
"Nothing reported near here in the last {hours} h": "لم يُبلَّغ عن شيء قريب من هنا في آخر {hours} ساعة",
"Latest news": "آخر الأخبار",
"No news matches these filters in the last {hours} h": "لا أخبار تطابق هذه المرشحات في آخر {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": "اللون: الخطورة · الرمز: نوع الحدث",
@@ -442,7 +442,7 @@
"AVAILABLE MISSIONS": "المهام المتاحة",
"SELECT A MISSION TO INSPECT": "اختر مهمة لفحصها",
"LOADING 30-DAY MISSION INDEX": "جارٍ تحميل فهرس مهام 30 يومًا",
"Space Missions unavailable: the mission index could not load. Try again later.": "المهام الفضائية غير متاحة: تعذّر تحميل فهرس المهام. أعد المحاولة لاحقًا.",
"Space Missions unavailable: the mission index could not load. Try again later.": "المهام الفضائية غير متاحة: تعذّر تحميل فهرس المهام. حاول مرة أخرى لاحقًا.",
"30-DAY MISSION INDEX UNAVAILABLE": "فهرس مهام 30 يومًا غير متاح",
"RADIO": "الراديو",
"ENABLE": "تفعيل",
@@ -536,12 +536,12 @@
"PLACE / CONDITIONS / POSITION": "المكان / الأحوال / الموضع",
"POSITION UNAVAILABLE": "الموضع غير متاح",
"REGIONAL NEWS UNAVAILABLE": "الأخبار الإقليمية غير متاحة",
"CACHED · REFRESH FAILED": "مخزّنة · فشل التحديث",
"CACHED · REFRESH FAILED": "مخزنة · فشل التحديث",
"REGION UNAVAILABLE": "المنطقة غير متاحة",
"NO NEWS IN THE LAST {hours} H": "لا أخبار خلال آخر {hours} س",
"SOURCE": "المصدر",
"{domain} · {age}": "{domain} · {age}",
"{source} · CACHED {age}": "{source} · مخزّنة {age}",
"{source} · CACHED {age}": "{source} · مخزنة {age}",
"TIME UNKNOWN": "الوقت غير معروف",
"{count}M AGO": "قبل {count} د",
"{count}H AGO": "قبل {count} س",
@@ -3385,7 +3385,7 @@
"Reset map bearing to north": "إعادة اتجاه الخريطة إلى الشمال",
"Malformed cyclone snapshot": "لقطة الأعاصير مشوّهة",
"Cyclone request timed out": "انتهت مهلة طلب الأعاصير",
"Regional sample · 250 nm radius · cells from visited views over {minutes} min": "عيّنة إقليمية · نصف قطر 250 ميلًا بحريًا · خلايا من المناطق التي استعرضتها خلال {minutes} د",
"Regional sample · 250 nm radius · cells from visited views over {minutes} min": "عيّنة إقليمية · نصف قطر 250 ميلًا بحريًا · خلايا من المناظر التي زرتها خلال {minutes} د",
"RADAR · {units}": "رادار · {units}",
"Color: {units}": "اللون: {units}",
"Left panels": "اللوحات اليمنى",
@@ -3411,7 +3411,7 @@
"Add the current view, style and layers as the next shot": "إضافة العرض الحالي والنمط والطبقات كلقطة تالية",
"+ CAPTURE SHOT": "+ التقاط لقطة",
"Replace the selected shot with the current view": "استبدال اللقطة المحددة بالعرض الحالي",
"Click a shot to select it, double-click to rename.": "انقر على لقطة لتحديدها، وانقر عليها نقرًا مزدوجًا لإعادة تسميتها.",
"Click a shot to select it, double-click to rename.": "انقر لقطة لتحديدها، وانقر نقرًا مزدوجًا لإعادة تسميتها.",
"Play from this scene through the rest of the project": "تشغيل من هذا المشهد حتى نهاية المشروع",
"Stop playback (Esc)": "إيقاف التشغيل (Esc)",
"Play the shot after the selected one, moving on to the next scene when needed": "تشغيل اللقطة التي تلي المحددة، مع الانتقال إلى المشهد التالي عند الحاجة",
@@ -3618,7 +3618,7 @@
"Within {km} km of the view centre": "ضمن {km} كم من مركز العرض",
"{region}, {country}": "{region}، {country}",
"via {source}": "عبر {source}",
"Local in the News layer: the day’s news from the outlets of the country in view": "قسم «محلي» في طبقة الأخبار: أخبار اليوم من وسائل إعلام البلد المعروض",
"Local in the News layer: the day’s news from the outlets of the country in view": "المحلي في طبقة الأخبار: أخبار اليوم من وسائل إعلام البلد المعروض",
"{provider} refused its key": "رفض {provider} مفتاحه",
"Check {envVars} in Provider Settings": "تحقق من {envVars} في «إعدادات المزوّدين»",
"{provider} key required": "مفتاح {provider} مطلوب",
@@ -3633,7 +3633,7 @@
"Date unknown": "التاريخ غير معروف",
"Position: centre of the protected area": "الموقع: مركز المنطقة المحمية",
"Position approximate (within about {km} km)": "الموقع تقريبي (في حدود {km} كم تقريبًا)",
"Position surveyed (within about {m} m)": "الموقع محدَّد بالمسح (في حدود {m} م تقريبًا)",
"Position surveyed (within about {m} m)": "الموقع مُسَح (في حدود {m} م تقريبًا)",
"Vessel: {value}": "السفينة: {value}",
"What happened: {value}": "ما حدث: {value}",
"Depth: {m} m ({quality})": "العمق: {m} م ({quality})",
@@ -3644,24 +3644,24 @@
"Source: {value}": "المصدر: {value}",
"Click the card to open EMODnet ↗": "انقر على البطاقة لفتح EMODnet ↗",
"Click the card to open the source ↗": "انقر على البطاقة لفتح المصدر ↗",
"Wikidata unavailable; showing EMODnet only": "Wikidata غير متاح؛ يُعرض EMODnet فقط",
"EMODnet unavailable; showing Wikidata only": "EMODnet غير متاح؛ يُعرض Wikidata فقط",
"Wikidata unavailable; showing EMODnet only": "ويكي بيانات غير متاحة؛ يُعرض EMODnet فقط",
"EMODnet unavailable; showing Wikidata only": "EMODnet غير متاح؛ تُعرض ويكي بيانات فقط",
"Shipwrecks unavailable": "بيانات حطام السفن غير متاحة",
"EMODnet (European waters) · Wikidata (worldwide)": "EMODnet (المياه الأوروبية) · Wikidata (العالم)",
"EMODnet (European waters) · Wikidata (worldwide)": "EMODnet (المياه الأوروبية) · ويكي بيانات (العالم)",
"{count} wrecks": {
"zero": "{count} سفينة غارقة",
"one": "سفينة غارقة واحدة ({count})",
"two": "سفينتان غارقتان ({count})",
"few": "{count} سفن غارقة",
"many": "{count} سفينة غارقة",
"other": "{count} سفينة غارقة"
"zero": "لا حطام ({count})",
"one": "حطام واحد ({count})",
"two": "حطامان ({count})",
"few": "{count} حطامات",
"many": "{count} حطامًا",
"other": "{count} حطام"
},
"1900–1945": "1900–1945",
"Shipwreck": "حطام سفينة",
"Shipwreck": "غرق سفينة",
"Fatalities: {count}": "الوفيات: {count}",
"Survivors: {count}": "الناجون: {count}",
"Click the card to open the article ↗": "انقر على البطاقة لفتح المقالة ↗",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "يأتي حطام السفن التراثي الأوروبي من EMODnet Human Activities، ويأتي الحطام البارز في العالم من Wikidata. يبيّن اللون متى غرقت السفينة، ويكبر حجم العلامة مع عدد الوفيات. انقر على علامة لعرض بطاقتها.",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "يأتي حطام السفن التراثي الأوروبي من EMODnet Human Activities، ويأتي الحطام البارز في العالم من ويكي بيانات. يبيّن اللون متى غرقت السفينة، ويكبر حجم العلامة مع عدد الوفيات. انقر على علامة لعرض بطاقتها.",
"All years": "كل السنوات",
"Period": "الفترة",
"From": "من",
@@ -3689,9 +3689,9 @@
"Other or unknown": "أخرى أو غير معروفة",
"{label} {detail} — click to retry": "{label} {detail} — انقر لإعادة المحاولة",
"Wildlife sightings source unavailable": "مصدر مشاهدات الحياة البرية غير متاح",
"Show only community-confirmed (research-grade) sightings": "إظهار المشاهدات المؤكدة مجتمعيًا (بدرجة بحثية) فقط",
"No recent sightings in view": "لا توجد مشاهدات حديثة في نطاق العرض",
"Acartia feed unavailable": "مصدر Acartia غير متاح",
"Show only community-confirmed (research-grade) sightings": "إظهار المشاهدات المؤكدة مجتمعياً (بدرجة بحثية) فقط",
"No recent sightings in view": "لا توجد مشاهدات حديثة في العرض",
"Acartia feed unavailable": "تعذر الوصول إلى مصدر Acartia",
"Show all taxa": "إظهار جميع المجموعات التصنيفية",
"BIRDS": "الطيور",
"Show only birds": "إظهار الطيور فقط",
@@ -3706,31 +3706,31 @@
"All rights reserved": "جميع الحقوق محفوظة",
"Cetacean": "حوتي",
"Wildlife Sightings": "مشاهدات الحياة البرية",
"Exit cockpit to fly to a sighting": "اخرج من القمرة للانتقال إلى مشاهدة",
"Exit cockpit to fly to a sighting area": "اخرج من القمرة للانتقال إلى منطقة مشاهدة",
"Exit cockpit to fly to a sighting": "اخرج من قمرة القيادة للانتقال إلى مشاهدة",
"Exit cockpit to fly to a sighting area": "اخرج من قمرة القيادة للانتقال إلى منطقة مشاهدة",
"Wildlife": "الحياة البرية",
"Zoom in for local sightings": "قرّب لرؤية المشاهدات المحلية",
"Loading photo...": "جارٍ تحميل الصورة…",
"Photo unavailable": "الصورة غير متاحة",
"Location obscured to a 0.2° cell": "الموقع مموّه ضمن خلية بحجم 0.2°",
"Location obscured to a 0.2° cell": "الموقع مخفي ضمن خلية بمقدار 0.2°",
"Photo of {name}": "صورة {name}",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": "المشاهدات في نطاق العرض: {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°. ليست إحصاءً شاملًا.",
"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",
"Open {name} on Acartia": "فتح {name} على Acartia",
"iNaturalist": "iNaturalist",
"iNaturalist contributors · licences per record": "مساهمو iNaturalist · التراخيص لكل سجل",
"iNaturalist contributors · Acartia {count} (Salish Sea) · licences per record": "مساهمو iNaturalist · Acartia {count} (بحر ساليش) · التراخيص لكل سجل",
"{days}d": "{days} ي",
"{days}d": "{days}ي",
"All markers": "كل العلامات",
"Show markers of every age": "عرض العلامات من جميع الأعمار",
"Maximum event age: {days}d; undated markers are kept": "الحد الأقصى لعمر الحدث: {days} ي؛ تبقى العلامات غير المؤرخة ظاهرة",
"Maximum event age: {days}d; undated markers are kept": "الحد الأقصى لعمر الحدث: {days} يوم؛ تُحفظ العلامات غير المؤرخة",
"event age filter|All": "الكل",
"Event age": "عمر الحدث",
"Event age: {days}d · undated kept": "عمر الحدث: {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.": "تستخدم صورة الخريطة طلبًا منفصلًا وقد تعرض لحظة مختلفة.",
@@ -3755,11 +3755,11 @@
"STARLINK": "STARLINK",
"The Starlink constellation (thousands of extra points)": "كوكبة Starlink (آلاف النقاط الإضافية)",
"PROBES": "المسابير",
"Space science, technology demonstration and other payloads": "علوم الفضاء واختبار التقنيات وحمولات أخرى",
"Space science, technology demonstration and other payloads": "علوم الفضاء والعروض التقنية وحمولات أخرى",
"DEBRIS": "الحطام",
"Rocket bodies, fragments and dead satellites": "مراحل الصواريخ والشظايا والأقمار المعطلة",
"Classes may lag: SATCAT is a cached copy": "قد لا تكون الفئات محدّثة: SATCAT نسخة مخزّنة",
"Classes may lag: SATCAT unavailable": "قد لا تكون الفئات محدّثة: SATCAT غير متاح",
"Classes may lag: SATCAT is a cached copy": "قد تتأخر الفئات: SATCAT نسخة مخزنة",
"Classes may lag: SATCAT unavailable": "قد تتأخر الفئات: SATCAT غير متاح",
"CelesTrak SATCAT gives each object's type and status. Without a current copy the classes rest on the UCS database, names and CelesTrak groups, so a satellite that has died since may still show as working.": "يعطي SATCAT من CelesTrak نوع كل جسم وحالته. من دون نسخة حديثة تستند الفئات إلى قاعدة بيانات UCS والأسماء ومجموعات CelesTrak، فقد يظهر قمر صناعي توقف عن العمل منذ ذلك الحين وكأنه يعمل.",
"STARLINK ···": "STARLINK ···",
"STARLINK ✕": "STARLINK ✕",
@@ -3780,7 +3780,7 @@
"Zoom out (−)": "تصغير (−)",
"ORBITAL VIEW · OFF-NADIR": "العرض المداري · انحراف عن النظير",
"World time": "وقت العالم",
"Back to live": "العودة إلى الوضع المباشر",
"Back to live": "العودة إلى البث المباشر",
"Live": "مباشر",
"1 h/s": "ساعة/ث",
"1 d/s": "يوم/ث",
@@ -3789,23 +3789,21 @@
"Scroll or pinch to zoom, drag to move along · double-click to see it all": "مرّر أو اقرص للتكبير، واسحب للتنقل · انقر نقرًا مزدوجًا لرؤية الكل",
"1 min/s": "دقيقة/ث",
"Rail": "السكك الحديدية",
"Urban rail": "القطارات الحضرية",
"Urban rail": "قطارات حضرية",
"Local rail": "قطارات الضواحي",
"Regional rail": "القطارات الإقليمية",
"National rail": "القطارات الوطنية",
"International rail": "القطارات الدولية",
"Regional rail": "قطارات إقليمية",
"National rail": "قطارات وطنية",
"International rail": "قطارات دولية",
"Hide {category}": "إخفاء {category}",
"Show {category}": "إظهار {category}",
"Live GPS": "GPS مباشر",
"Estimated from station reports": "مقدَّر من بلاغات المحطات",
"To {headsign}": "إلى {headsign}",
"Next {stop} {time} {relative}": "المحطة التالية: {stop} {time} {relative}",
"Prev {stop} {time} {relative}": "المحطة السابقة: {stop} {time} {relative}",
"Next {stop} {time} {relative}": "المحطة التالية {stop} {time} {relative}",
"Prev {stop} {time} {relative}": "المحطة السابقة {stop} {time} {relative}",
"(+{minutes} min late)": "(تأخير {minutes} د)",
"Exit cockpit to fly to a train": "اخرج من القمرة للانتقال إلى قطار",
"Photo © {photographer}": "صورة © {photographer}",
"Photo: {photographer} · {licence}": "صورة: {photographer} · {licence}",
"Photo · {licence}": "صورة · {licence}",
"The shared flight isn't reporting now — showing where it was last seen": "الرحلة المشتركة: لا بيانات واردة الآن — يُعرض آخر موضع معروف",
"The shared flight isn't reporting now — showing where it was at {time}": "الرحلة المشتركة: لا بيانات واردة الآن — يُعرض موضعها عند {time}",
"The shared military flight isn't reporting now — showing where it was last seen": "الرحلة العسكرية المشتركة: لا بيانات واردة الآن — يُعرض آخر موضع معروف",
@@ -3813,16 +3811,16 @@
"The shared vessel isn't reporting now — showing where it was last seen": "السفينة المشتركة: لا بيانات واردة الآن — يُعرض آخر موضع معروف",
"The shared vessel isn't reporting now — showing where it was at {time}": "السفينة المشتركة: لا بيانات واردة الآن — يُعرض موضعها عند {time}",
"The shared satellite isn't reporting now — showing where it was last seen": "القمر الصناعي المشترك: لا بيانات واردة الآن — يُعرض آخر موضع معروف",
"The shared satellite isn't reporting now — showing where it was at {time}": "القمر الصناعي المشترك: لا بيانات واردة الآن — يُعرض موضعه عند {time}",
"The shared satellite isn't reporting now — showing where it was at {time}": "القمر الصناعي المشترك: لا بيانات واردة الآن — يُعرض موضعها عند {time}",
"ICAO: {id}": "ICAO: {id}",
"NORAD: {id}": "NORAD: {id}",
"MMSI: {id}": "MMSI: {id}",
"Vessel UUID: {id}": "معرّف UUID للسفينة: {id}",
"Latitude must be within ±90° and longitude within ±180°": "يجب أن يكون خط العرض ضمن ±90° وخط الطول ضمن ±180°",
"Street Level is unavailable": "«مستوى الشارع» غير متاح",
"Street Level is unavailable": "مستوى الشارع غير متاح",
"No street-level imagery source is configured": "لم يُعَدّ أي مصدر لصور مستوى الشارع",
"Street Level could not be enabled": "تعذّر تفعيل «مستوى الشارع»",
"Street Level changed while it looked for imagery": "تغيّرت حالة «مستوى الشارع» أثناء البحث عن الصور",
"Street Level could not be enabled": "تعذّر تفعيل مستوى الشارع",
"Street Level changed while it looked for imagery": "تغيّر مستوى الشارع أثناء البحث عن الصور",
"Aim the view at a street": "وجّه العرض نحو شارع",
"{source} is not a street-level imagery source here": "{source} ليس مصدرًا لصور مستوى الشارع هنا",
"{source} needs its key": "يحتاج {source} إلى مفتاحه",
@@ -3830,13 +3828,13 @@
"{source} could not be switched on": "تعذّر تفعيل {source}",
"{source} is not available on this map": "{source} غير متاح على هذه الخريطة",
"{source} imagery could not be opened": "تعذّر فتح صور {source}",
"{place} has no position to look for imagery at": "{place}: لا موضع للبحث عن الصور فيه",
"{place} has no position to look for imagery at": "لا يوجد لـ{place} موضع للبحث عن الصور فيه",
"No {source} imagery near {place}": "لا توجد صور {source} قرب {place}",
"{reason} (filters on: {filters})": "{reason} (المرشحات المفعّلة: {filters})",
"tiles unavailable": "البلاطات غير متاحة",
"zoom in to load this layer": "قرّب لتحميل هذه الطبقة",
"zoom in to load this layer": "كبّر لتحميل هذه الطبقة",
"Zoom to layer": "تكبير إلى الطبقة",
"Zoom to layer · fly to where this layer has data": "تكبير إلى الطبقة · الانتقال إلى حيث توجد بيانات هذه الطبقة",
"Zoom to layer · fly to where this layer has data": "تكبير إلى الطبقة · الطيران إلى حيث توجد بيانات هذه الطبقة",
"Remove from Data Layers": "إزالة من طبقات البيانات",
"Remove from Data Layers · add it again from the library": "إزالة من طبقات البيانات · أضِفها مجددًا من المكتبة",
"Coverage outline": "حدود التغطية",
@@ -3847,7 +3845,7 @@
"1,000–2,000 m": "1,000–2,000 م",
"2,000–3,000 m": "2,000–3,000 م",
"3,000 m and above": "3,000 م فأكثر",
"Exit cockpit to fly to a layer": "اخرج من القمرة للانتقال إلى طبقة",
"Exit cockpit to fly to a layer": "اخرج من قمرة القيادة للطيران إلى طبقة",
"Library": "المكتبة",
"Environment": "البيئة",
"Imagery": "الصور",
@@ -3863,24 +3861,24 @@
"{datasets} · {tiles} · {packs}": "{datasets} · {tiles} · {packs}",
"{count} datasets": {
"zero": "{count} مجموعة بيانات",
"one": "مجموعة بيانات واحدة ({count})",
"two": "مجموعتا بيانات ({count})",
"one": "{count} مجموعة بيانات",
"two": "{count} مجموعتا بيانات",
"few": "{count} مجموعات بيانات",
"many": "{count} مجموعة بيانات",
"other": "{count} مجموعة بيانات"
},
"{count} tile layers": {
"zero": "{count} طبقة بلاطات",
"one": "طبقة بلاطات واحدة ({count})",
"two": "طبقتا بلاطات ({count})",
"one": "{count} طبقة بلاطات",
"two": "{count} طبقتا بلاطات",
"few": "{count} طبقات بلاطات",
"many": "{count} طبقة بلاطات",
"other": "{count} طبقة بلاطات"
},
"{count} packs": {
"zero": "{count} حزمة",
"one": "حزمة واحدة ({count})",
"two": "حزمتان ({count})",
"one": "{count} حزمة",
"two": "{count} حزمتان",
"few": "{count} حزم",
"many": "{count} حزمة",
"other": "{count} حزمة"
@@ -3899,8 +3897,8 @@
"Map features": "معالم الخريطة",
"from {count} datasets": {
"zero": "من {count} مجموعة بيانات",
"one": "من مجموعة بيانات واحدة ({count})",
"two": "من مجموعتَي بيانات ({count})",
"one": "من {count} مجموعة بيانات",
"two": "من {count} مجموعتَي بيانات",
"few": "من {count} مجموعات بيانات",
"many": "من {count} مجموعة بيانات",
"other": "من {count} مجموعة بيانات"
@@ -3923,7 +3921,7 @@
"Resolution": "الدقة",
"Flown": "تاريخ التصوير",
"Updates": "التحديث",
"Ortho quality": "جودة التصحيح التعامدي",
"Ortho quality": "جودة التعامد",
"DATA LIBRARY": "مكتبة البيانات",
"Close data library": "إغلاق مكتبة البيانات",
"Data Library at a glance": "مكتبة البيانات في لمحة",
@@ -3932,7 +3930,7 @@
"Theme": "الموضوع",
"All themes": "كل المواضيع",
"Only what this instance serves": "فقط ما يقدّمه هذا المثيل",
"OPT-IN": "بتفعيل المشغّل",
"OPT-IN": "اختياري",
"KEY": "مفتاح",
"KEYLESS": "بلا مفتاح",
"ZOOM": "تكبير",
+57 -59
View File
@@ -1,6 +1,6 @@
{
"News feed unavailable": "Nachrichtenfeed nicht verfügbar",
"Developing: {name}": "Aktuelle Entwicklung: {name}",
"Developing: {name}": "In Entwicklung: {name}",
"{count} events today": {
"one": "{count} Ereignis heute",
"other": "{count} Ereignisse heute"
@@ -13,11 +13,11 @@
"Video": "Video",
"Photos": "Fotos",
"Claimed by {who}": "Behauptet von {who}",
"Claimed": "Behauptet",
"Claimed": "Behauptung",
"Not yet confirmed by others": "Noch nicht von anderen bestätigt",
"Reported by {side} sources": "Gemeldet von Quellen: {side}",
"Reported by {sides}": "Gemeldet von Quellen: {sides}",
"{side} sources": "{side}",
"Reported by {side} sources": "Gemeldet von Quellen aus {side}",
"Reported by {sides}": "Gemeldet von {sides}",
"{side} sources": "Quellen aus {side}",
"Official": "Offiziell",
"Reported by both sides": "Von beiden Seiten gemeldet",
"Independently reported": "Unabhängig gemeldet",
@@ -47,8 +47,8 @@
"Significant": "Erheblich",
"Major": "Schwer",
"age|now": "jetzt",
"{minutes} min": "{minutes} min",
"{hours} h": "{hours} h",
"{minutes} min": "{minutes} Min.",
"{hours} h": "{hours} Std.",
"{count} sources": {
"one": "{count} Quelle",
"other": "{count} Quellen"
@@ -65,8 +65,8 @@
"{severity} only": "Nur {severity}",
"All categories": "Alle Kategorien",
"Reading the news…": "Nachrichten werden gelesen…",
"News, last {hours} hours": "Nachrichten, letzte {hours} h",
"Last {hours} h": "Letzte {hours} h",
"News, last {hours} hours": "Nachrichten der letzten {hours} Stunden",
"Last {hours} h": "Letzte {hours} Std.",
"Time window": "Zeitraum",
"Conflicts": "Konflikte",
"Local": "Lokal",
@@ -82,8 +82,8 @@
"How to read the map": "So liest man die Karte",
"Colour: severity · icon: type of event": "Farbe: Schweregrad · Symbol: Art des Ereignisses",
"Solid rim: reported by both sides or independently. Dashed rim: a claim. Thin rim: one source, or one side’s own press.": "Durchgehender Rand: von beiden Seiten oder unabhängig gemeldet. Gestrichelter Rand: eine Behauptung. Dünner Rand: eine Quelle oder die eigene Presse einer Seite.",
"Larger: more sources · fainter: older. Picture card: a major or fast-growing event with two or more sources.": "Größer: mehr Quellen · blasser: älter. Infokarte mit Bild: ein schweres oder schnell wachsendes Ereignis mit zwei oder mehr Quellen.",
"Side labels describe the source, not whether a report is true.": "Angaben zur Seite beschreiben die Quelle, nicht ob eine Meldung stimmt.",
"Larger: more sources · fainter: older. Picture card: a major or fast-growing event with two or more sources.": "Größer: mehr Quellen · blasser: älter. Bildkarte: ein schweres oder schnell wachsendes Ereignis mit zwei oder mehr Quellen.",
"Side labels describe the source, not whether a report is true.": "Seitenangaben beschreiben die Quelle, nicht ob eine Meldung stimmt.",
"Exit cockpit to fly to a news report": "Cockpit verlassen, um zu einer Meldung zu fliegen",
"News items in the feed": "Meldungen im Feed",
"Flying to Austin, TX...": "Flug nach Austin, TX...",
@@ -252,7 +252,7 @@
"loading...": "lädt...",
"KEY REQUIRED": "SCHLÜSSEL ERFORDERLICH",
"LIVE · updated {age}": "LIVE · aktualisiert {age}",
"24 h to {day} {time} UTC": "24 h bis {day} {time} UTC",
"24 h to {day} {time} UTC": "24 Std. bis {day} {time} UTC",
"loading mapped installation context": "Kontext der kartierten Anlage wird geladen",
"MAPPED INSTALLATION": "KARTIERTE ANLAGE",
"LAUNCH SITE · {site}": "STARTPLATZ · {site}",
@@ -282,15 +282,15 @@
"{value} KM/S": "{value} KM/S",
"{direction} {angle}°": "{direction} {angle}°",
"OFF-NADIR {angle}° · {offset}": "NADIRABWEICHUNG {angle}° · {offset}",
"SITES STANDBY": "STANDORTE STANDBY",
"ENABLE MAPPED INSTALLATIONS FOR SITE PINGS": "KARTIERTE ANLAGEN FÜR STANDORT-PINGS AKTIVIEREN",
"SITES STANDBY": "ORTE STANDBY",
"ENABLE MAPPED INSTALLATIONS FOR SITE PINGS": "KARTIERTE ANLAGEN FÜR ORTS-PINGS AKTIVIEREN",
"Release the camera from {name}": "Kamera von {name} lösen",
"RETURN · {name}": "ZURÜCK · {name}",
"PREVIOUS SATELLITE": "VORHERIGER SATELLIT",
"Return to {name}": "Zurück zu {name}",
"SATELLITE": "SATELLIT",
"Ride {name}": "Mit {name} fliegen",
"Unnamed site": "Unbenannter Standort",
"Unnamed site": "Unbenannter Ort",
"OFF-NADIR {angle}°": "NADIRABWEICHUNG {angle}°",
"FOCUS · {name}": "FOKUS · {name}",
"Keep the camera on {name}": "Kamera auf {name} halten",
@@ -493,8 +493,8 @@
"other": "{count} Kameras geladen · CCTV aktivieren"
},
"{count} more cameras outside this view · pan or zoom out": {
"one": "{count} weitere Kamera außerhalb dieser Ansicht · Karte verschieben oder herauszoomen",
"other": "{count} weitere Kameras außerhalb dieser Ansicht · Karte verschieben oder herauszoomen"
"one": "{count} weitere Kamera außerhalb dieser Ansicht · verschieben oder herauszoomen",
"other": "{count} weitere Kameras außerhalb dieser Ansicht · verschieben oder herauszoomen"
},
"Enable CCTV to load camera intersections": "CCTV aktivieren, um Kreuzungskameras zu laden",
"Enable CCTV to start camera-linked intelligence summaries.": "CCTV aktivieren, um kameragestützte Lagezusammenfassungen zu starten.",
@@ -1619,7 +1619,7 @@
"Low-accuracy score 2–10%": "Ungenauigkeitswert 2–10 %",
"Low-accuracy score > 10%": "Ungenauigkeitswert > 10 %",
"GNSS integrity source unavailable": "GNSS-Integritätsquelle nicht verfügbar",
"Regional sample · 250 nm radius · cells from visited views over {minutes} min": "Regionale Stichprobe · Radius 250 NM · Zellen aus besuchten Ansichten der letzten {minutes} min",
"Regional sample · 250 nm radius · cells from visited views over {minutes} min": "Regionale Stichprobe · Radius 250 NM · Zellen aus besuchten Ansichten über {minutes} min",
"Retrying automatically at {time} UTC": "Automatischer neuer Versuch um {time} UTC",
"Retrying automatically after provider cooldown": "Automatischer neuer Versuch nach der Sperrzeit des Anbieters",
"Updates automatically after pan or zoom and once a minute. The public feed covers a 250-nautical-mile circle around the view centre; zooming out does not make it a worldwide snapshot. Blank cells are outside sampled coverage or have insufficient observations.": "Aktualisiert sich nach Schwenken oder Zoomen und einmal pro Minute automatisch. Der öffentliche Feed deckt einen Kreis von 250 Seemeilen um die Bildmitte ab; Herauszoomen macht daraus keinen weltweiten Snapshot. Leere Zellen liegen außerhalb der Stichprobe oder haben zu wenige Beobachtungen.",
@@ -3107,7 +3107,7 @@
"Restricted": "Flugbeschränkungsgebiet",
"Prohibited & Restricted Airspace": "Sperr- und Flugbeschränkungsgebiete",
"scene|PLAY": "START",
"AUTHORING": "SZENEN",
"AUTHORING": "ERSTELLUNG",
"Create and edit scenes": "Szenen erstellen und bearbeiten",
"SCENE": "SZENE",
"Scene": "Szene",
@@ -3129,8 +3129,8 @@
"Download the timing log of the last play": "Zeitprotokoll der letzten Wiedergabe herunterladen",
"Edit anchors, data packs and shot details": "Anker, Datenpakete und Aufnahmedetails bearbeiten",
"Download this scene as JSON or an asset bundle": "Diese Szene als JSON oder Asset-Bundle herunterladen",
"Left panels": "Linke Bereiche",
"Right panels": "Rechte Bereiche",
"Left panels": "Linke Panels",
"Right panels": "Rechte Panels",
"CONFIRMED": "BESTÄTIGT",
"PAD / RTLS": "STARTPLATZ / RTLS",
"fire confidence filter|All": "Alle",
@@ -3292,20 +3292,20 @@
"Too many chat requests. Try again in {seconds} s.": "Zu viele Chat-Anfragen. In {seconds} s erneut versuchen.",
"The provider refused access for this key. Check the key's project, region or permissions.": "Der Anbieter hat den Zugriff für diesen Schlüssel verweigert. Projekt, Region oder Berechtigungen des Schlüssels prüfen.",
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "Das Anbieterkonto hat kein Guthaben mehr. Die Abrechnung prüfen oder in den Einstellungen einen anderen KI-Anbieter wählen.",
"Last 24 h of news. Each report links to its publisher; places and categories are read from the text and data automatically and can be wrong. Side labels, where shown, describe the source, not whether a report is true.": "Nachrichten der letzten 24 h. Jede Meldung verlinkt auf ihren Herausgeber; Orte und Kategorien werden automatisch aus Text und Daten gelesen und können falsch sein. Angaben zur Seite, wo vorhanden, beschreiben die Quelle, nicht ob eine Meldung stimmt.",
"Last 24 h of news. Each report links to its publisher; places and categories are read from the text and data automatically and can be wrong. Side labels, where shown, describe the source, not whether a report is true.": "Letzte 24 h Nachrichten. Jede Meldung verlinkt auf ihren Herausgeber; Orte und Kategorien werden automatisch aus Text und Daten gelesen und können falsch sein. Seitenangaben, wo vorhanden, beschreiben die Quelle, nicht ob eine Meldung stimmt.",
"Economy": "Wirtschaft",
"News from {area}": "Nachrichten: {area}",
"Within {km} km of the view centre": "Im Umkreis von {km} km um die Bildmitte",
"{region}, {country}": "{region}, {country}",
"via {source}": "über {source}",
"Local in the News layer: the day’s news from the outlets of the country in view": "„Lokal“ in der Ebene „Nachrichten“: die Nachrichten des Tages aus den Medien des Landes im Bild",
"Local in the News layer: the day’s news from the outlets of the country in view": "Lokal in der Nachrichtenebene: die Nachrichten des Tages aus den Medien des Landes im Blick",
"{provider} refused its key": "{provider} hat den Schlüssel abgelehnt",
"Check {envVars} in Provider Settings": "{envVars} in den Anbieter-Einstellungen prüfen",
"{provider} key required": "Schlüssel für {provider} erforderlich",
"{provider} key required": "{provider}-Schlüssel erforderlich",
"{provider} limit reached for today": "{provider}: Tageslimit erreicht",
"News resumes tomorrow, or with a paid plan": "Nachrichten gibt es morgen wieder, oder mit einem kostenpflichtigen Tarif",
"News key required": "Nachrichtenschlüssel erforderlich",
"Precise conflict points in the News layer, beside GDELT’s articles": "Genaue Konfliktpunkte in der Ebene „Nachrichten“, neben den Artikeln von GDELT",
"Precise conflict points in the News layer, beside GDELT’s articles": "Genaue Konfliktpunkte in der Nachrichtenebene, neben den Artikeln von GDELT",
"Worldwide": "Weltweit",
"Shipwrecks": "Schiffswracks",
"Before 1900": "Vor 1900",
@@ -3322,8 +3322,8 @@
"Cargo or finds: {value}": "Ladung oder Funde: {value}",
"Protected wreck": "Geschütztes Wrack",
"Source: {value}": "Quelle: {value}",
"Click the card to open EMODnet ↗": "Eintrag anklicken, um EMODnet zu öffnen ↗",
"Click the card to open the source ↗": "Eintrag anklicken, um die Quelle zu öffnen ↗",
"Click the card to open EMODnet ↗": "Karte anklicken, um EMODnet zu öffnen ↗",
"Click the card to open the source ↗": "Karte anklicken, um die Quelle zu öffnen ↗",
"Wikidata unavailable; showing EMODnet only": "Wikidata nicht verfügbar; nur EMODnet wird angezeigt",
"EMODnet unavailable; showing Wikidata only": "EMODnet nicht verfügbar; nur Wikidata wird angezeigt",
"Shipwrecks unavailable": "Schiffswracks nicht verfügbar",
@@ -3333,11 +3333,11 @@
"other": "{count} Wracks"
},
"1900–1945": "1900–1945",
"Shipwreck": "Schiffswrack",
"Shipwreck": "Schiffbruch",
"Fatalities: {count}": "Todesopfer: {count}",
"Survivors: {count}": "Überlebende: {count}",
"Click the card to open the article ↗": "Eintrag anklicken, um den Artikel zu öffnen ↗",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Europäische Kulturerbe-Wracks stammen von EMODnet Human Activities, bekannte Wracks weltweit von Wikidata. Die Farbe zeigt, wann das Schiff sank; die Markierung wächst mit der Zahl der Todesopfer. Für die Infokarte eine Markierung anklicken.",
"Click the card to open the article ↗": "Karte anklicken, um den Artikel zu öffnen ↗",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Europäische Kulturerbe-Wracks stammen von EMODnet Human Activities, bekannte Wracks weltweit von Wikidata. Die Farbe zeigt, wann das Schiff sank; die Markierung wächst mit der Zahl der Todesopfer. Klicke auf eine Markierung für ihre Karte.",
"All years": "Alle Jahre",
"Period": "Zeitraum",
"From": "Von",
@@ -3363,11 +3363,11 @@
"Fungi": "Pilze",
"Other animals": "Andere Tiere",
"Other or unknown": "Andere oder unbekannt",
"{label} {detail} — click to retry": "{label}: {detail} – zum erneuten Versuch klicken",
"{label} {detail} — click to retry": "{label} {detail} — zum Wiederholen klicken",
"Wildlife sightings source unavailable": "Quelle für Naturbeobachtungen nicht verfügbar",
"Show only community-confirmed (research-grade) sightings": "Nur gemeinschaftlich bestätigte Beobachtungen (Forschungsqualität) anzeigen",
"No recent sightings in view": "Keine aktuellen Beobachtungen im Bild",
"Acartia feed unavailable": "Acartia-Feed nicht verfügbar",
"No recent sightings in view": "Keine aktuellen Beobachtungen im Ausschnitt",
"Acartia feed unavailable": "Acartia-Datenstrom nicht verfügbar",
"Show all taxa": "Alle Artengruppen anzeigen",
"BIRDS": "VÖGEL",
"Show only birds": "Nur Vögel anzeigen",
@@ -3385,48 +3385,48 @@
"Exit cockpit to fly to a sighting": "Cockpit verlassen, um zu einer Beobachtung zu fliegen",
"Exit cockpit to fly to a sighting area": "Cockpit verlassen, um zu einem Beobachtungsgebiet zu fliegen",
"Wildlife": "Tierwelt",
"Zoom in for local sightings": "Hineinzoomen für lokale Beobachtungen",
"Zoom in for local sightings": "Für lokale Beobachtungen vergrößern",
"Loading photo...": "Foto wird geladen…",
"Photo unavailable": "Foto nicht verfügbar",
"Location obscured to a 0.2° cell": "Standort auf eine 0,2°-Rasterzelle verschleiert",
"Photo of {name}": "Foto von {name}",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{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.",
"{count} sightings in view": "{count} Sichtungen im Ausschnitt",
"Close sighting": "Sichtung 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 Abzeichensymbol 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",
"Open {name} on Acartia": "{name} auf Acartia öffnen",
"iNaturalist": "iNaturalist",
"iNaturalist contributors · licences per record": "iNaturalist-Beitragende · Lizenzen je Eintrag",
"iNaturalist contributors · Acartia {count} (Salish Sea) · licences per record": "iNaturalist-Beitragende · Acartia {count} (Salish-See) · Lizenzen je Eintrag",
"{days}d": "{days} T",
"{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": "Maximales Ereignisalter: {days} T; undatierte Markierungen bleiben erhalten",
"Maximum event age: {days}d; undated markers are kept": "Maximales Ereignisalter: {days} Tage; undatierte Markierungen bleiben",
"event age filter|All": "Alle",
"Event age": "Ereignisalter",
"Event age: {days}d · undated kept": "Ereignisalter: {days} T · undatierte bleiben",
"Event age: {days}d · undated kept": "Ereignisalter: {days} Tage · 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.",
"Video plays from Québec 511, which sees your IP address.": "Das Video wird von Québec 511 abgespielt, das Ihre IP-Adresse sieht.",
"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}",
"Street level at {place}": "Straßenebene: {place}",
"{source} at {place}": "{source} bei {place}",
"Street level at {place}": "Straßenebene bei {place}",
"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",
"{source} cannot open this link's street-level photo without a working key": "{source} kann das Foto auf Straßenebene aus diesem Link ohne gültigen Schlüssel nicht öffnen",
"{source} cannot open this link's street-level photo without a working key": "{source} kann das Straßenfoto dieses Links ohne gültigen Schlüssel nicht öffnen",
"Street-level photos are hidden in the current view. Leave it first.": "Fotos auf Straßenebene sind in dieser Ansicht ausgeblendet. Zuerst die Ansicht verlassen.",
"STATIONS": "STATIONEN",
"Crewed stations and the craft that visit them": "Bemannte Stationen und die Raumfahrzeuge, die sie anfliegen",
"IMAGING": "BILDGEBUNG",
"Earth observation: optical, radar, weather and science": "Erdbeobachtung: optisch, Radar, Wetter und Wissenschaft",
"TELECOM": "KOMM.",
"TELECOM": "TELEKOM",
"Communications and navigation satellites, Starlink excepted": "Kommunikations- und Navigationssatelliten, ohne Starlink",
"STARLINK": "STARLINK",
"The Starlink constellation (thousands of extra points)": "Die Starlink-Konstellation (Tausende zusätzliche Punkte)",
@@ -3443,11 +3443,11 @@
"SENSOR SWATH ≈ {value} KM": "SENSORSTREIFEN ≈ {value} KM",
"SENSOR": "SENSOR",
"Zoom to the imaging sensor swath": "Auf den Streifen des Bildsensors zoomen",
"<1 H OLD": "<1 H ALT",
"{value} H OLD": "{value} H ALT",
"<1 H OLD": "<1 STD ALT",
"{value} H OLD": "{value} STD ALT",
"{value} D OLD": "{value} T ALT",
"FWD": "VORAUS",
"AFT": "ACHTERAUS",
"AFT": "ACHTERN",
"RIGHT": "RECHTS",
"LEFT": "LINKS",
"TRACK UP": "KURS OBEN",
@@ -3456,17 +3456,17 @@
"Zoom out (−)": "Herauszoomen (−)",
"ORBITAL VIEW · OFF-NADIR": "ORBITALANSICHT · NADIRABWEICHUNG",
"World time": "Weltzeit",
"Back to live": "Zurück zu Live",
"Back to live": "Zurück zu live",
"Live": "Live",
"1 h/s": "1 h/s",
"1 h/s": "1 Std./s",
"1 d/s": "1 Tag/s",
"1 wk/s": "1 Wo./s",
"Play speed": "Wiedergabegeschwindigkeit",
"Scroll or pinch to zoom, drag to move along · double-click to see it all": "Scrollen oder mit zwei Fingern zoomen, ziehen zum Verschieben · Doppelklick zeigt alles",
"1 min/s": "1 min/s",
"1 min/s": "1 Min./s",
"Rail": "Bahn",
"Urban rail": "Stadtverkehr",
"Local rail": "Vorortverkehr",
"Local rail": "Nahverkehr",
"Regional rail": "Regionalverkehr",
"National rail": "Fernverkehr",
"International rail": "International",
@@ -3476,12 +3476,10 @@
"Estimated from station reports": "Geschätzt aus Bahnhofsmeldungen",
"To {headsign}": "Nach {headsign}",
"Next {stop} {time} {relative}": "Nächster Halt {stop} {time} {relative}",
"Prev {stop} {time} {relative}": "Vorheriger Halt {stop} {time} {relative}",
"Prev {stop} {time} {relative}": "Letzter Halt {stop} {time} {relative}",
"(+{minutes} min late)": "(+{minutes} min Verspätung)",
"Exit cockpit to fly to a train": "Cockpit verlassen, um zu einem Zug zu fliegen",
"Photo © {photographer}": "Foto © {photographer}",
"Photo: {photographer} · {licence}": "Foto: {photographer} · {licence}",
"Photo · {licence}": "Foto · {licence}",
"The shared flight isn't reporting now — showing where it was last seen": "Der geteilte Flug sendet gerade nicht – letzte bekannte Position wird gezeigt",
"The shared flight isn't reporting now — showing where it was at {time}": "Der geteilte Flug sendet gerade nicht – Position um {time} wird gezeigt",
"The shared military flight isn't reporting now — showing where it was last seen": "Der geteilte Militärflug sendet gerade nicht – letzte bekannte Position wird gezeigt",
@@ -3502,7 +3500,7 @@
"Aim the view at a street": "Die Ansicht auf eine Straße richten",
"{source} is not a street-level imagery source here": "{source} ist hier keine Quelle für Bilder auf Straßenebene",
"{source} needs its key": "{source} braucht seinen Schlüssel",
"{source} needs the {map} map, which is not available": "{source} braucht die Karte „{map}“, die nicht verfügbar ist",
"{source} needs the {map} map, which is not available": "{source} braucht die {map}-Karte, die nicht verfügbar ist",
"{source} could not be switched on": "{source} konnte nicht eingeschaltet werden",
"{source} is not available on this map": "{source} ist auf dieser Karte nicht verfügbar",
"{source} imagery could not be opened": "Bilder von {source} konnten nicht geöffnet werden",
@@ -3568,13 +3566,13 @@
"Turned off by this instance's operator": "Vom Betreiber dieser Instanz ausgeschaltet",
"Needs a provider key on the server": "Benötigt einen Anbieterschlüssel auf dem Server",
"Needs this app's server · not available in a static build": "Benötigt den Server dieser App · in einem statischen Build nicht verfügbar",
"Checking what this instance serves…": "Es wird geprüft, was diese Instanz bereitstellt…",
"Checking what this instance serves…": "Prüfe, was diese Instanz bereitstellt…",
"Regional": "Regional",
"Orthophoto": "Orthofoto",
"Satellite mosaic": "Satellitenmosaik",
"Oblique": "Schrägluftbild",
"Historic imagery": "Historische Luftbilder",
"True orthophoto": "True-Orthofoto",
"True orthophoto": "True Orthofoto",
"Type": "Typ",
"Resolution": "Auflösung",
"Flown": "Befliegung",
@@ -3583,7 +3581,7 @@
"DATA LIBRARY": "DATENBIBLIOTHEK",
"Close data library": "Datenbibliothek schließen",
"Data Library at a glance": "Datenbibliothek auf einen Blick",
"Live sources beyond the core layers. Each one is fetched from its provider while it is on; nothing is stored in this app. Add one and it appears in Data Layers under Library.": "Live-Quellen über die Kernebenen hinaus. Jede wird von ihrem Anbieter abgerufen, solange sie eingeschaltet ist; in dieser App wird nichts gespeichert. Hinzugefügte Quellen erscheinen in den Datenebenen unter „Bibliothek“.",
"Live sources beyond the core layers. Each one is fetched from its provider while it is on; nothing is stored in this app. Add one and it appears in Data Layers under Library.": "Live-Quellen über die Kernebenen hinaus. Jede wird von ihrem Anbieter abgerufen, solange sie eingeschaltet ist; in dieser App wird nichts gespeichert. Fügen Sie eine hinzu, und sie erscheint in den Datenebenen unter Bibliothek.",
"Name, provider or country": "Name, Anbieter oder Land",
"Theme": "Thema",
"All themes": "Alle Themen",
+58 -60
View File
@@ -3,12 +3,12 @@
"Developing: {name}": "En desarrollo: {name}",
"{count} events today": {
"one": "{count} suceso hoy",
"many": "{count} de sucesos hoy",
"many": "{count} sucesos hoy",
"other": "{count} sucesos hoy"
},
"{count} reports today": {
"one": "{count} noticia hoy",
"many": "{count} de noticias hoy",
"many": "{count} noticias hoy",
"other": "{count} noticias hoy"
},
"Disputed: {name}": "En disputa: {name}",
@@ -17,9 +17,9 @@
"Claimed by {who}": "Según {who}",
"Claimed": "Afirmación",
"Not yet confirmed by others": "Aún sin confirmar por otros",
"Reported by {side} sources": "Informado por fuentes: {side}",
"Reported by {sides}": "Informado por fuentes: {sides}",
"{side} sources": "{side}",
"Reported by {side} sources": "Informado por fuentes de {side}",
"Reported by {sides}": "Informado por {sides}",
"{side} sources": "fuentes de {side}",
"Official": "Oficial",
"Reported by both sides": "Informado por ambos bandos",
"Independently reported": "Informado de forma independiente",
@@ -43,7 +43,7 @@
"Humanitarian": "Humanitario",
"Disaster": "Catástrofe",
"Protest": "Protesta",
"Political": "Política",
"Political": "Político",
"No physical event": "Sin hecho físico",
"Minor incident": "Incidente menor",
"Significant": "Significativo",
@@ -53,34 +53,34 @@
"{hours} h": "{hours} h",
"{count} sources": {
"one": "{count} fuente",
"many": "{count} de fuentes",
"many": "{count} fuentes",
"other": "{count} fuentes"
},
"News feed not configured on this server": "Noticias no configuradas en este servidor",
"Malformed news hotspots response": "Respuesta de focos de noticias mal formada",
"Malformed news feed response": "Respuesta de noticias mal formada",
"{place} (placed by model)": "{place} (ubicado por el modelo)",
"English headline by the model": "Titular traducido al inglés por el modelo",
"English headline by the model": "Titular en inglés del modelo",
"Not placed on the map": "Sin ubicar en el mapa",
"Close": "Cerrar",
"Any severity": "Cualquier gravedad",
"{severity} and above": "{severity} o superior",
"{severity} and above": "{severity} o más",
"{severity} only": "Solo {severity}",
"All categories": "Todas las categorías",
"Reading the news…": "Leyendo las noticias…",
"News, last {hours} hours": "Noticias desde hace {hours} h",
"Last {hours} h": "Desde hace {hours} h",
"News, last {hours} hours": "Noticias de las últimas {hours} horas",
"Last {hours} h": "Últimas {hours} h",
"Time window": "Periodo",
"Conflicts": "Conflictos",
"Local": "Local",
"News sections": "Secciones de noticias",
"Zoom in to see local news": "Acércate para ver noticias locales",
"Zoom in to see local news": "Acerca el mapa para ver noticias locales",
"Hotspots": "Focos",
"Severity": "Gravedad",
"Category": "Categoría",
"Nothing reported near here in the last {hours} h": "No hay noticias cerca de aquí desde hace {hours} h",
"Nothing reported near here in the last {hours} h": "Nada informado cerca de aquí en las últimas {hours} h",
"Latest news": "Últimas noticias",
"No news matches these filters in the last {hours} h": "Desde hace {hours} h no hay noticias que coincidan con estos filtros",
"No news matches these filters in the last {hours} h": "Ninguna noticia de las últimas {hours} h coincide 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",
@@ -219,8 +219,8 @@
"Landmark: --": "Punto de interés: --",
"Search any location": "Buscar un lugar",
"Search any location...": "Buscar un lugar…",
"LOOK STBD {bearing}°": "VISTA A ESTRIBOR {bearing}°",
"LOOK PORT {bearing}°": "VISTA A BABOR {bearing}°",
"LOOK STBD {bearing}°": "VISTA ESTRIBOR {bearing}°",
"LOOK PORT {bearing}°": "VISTA BABOR {bearing}°",
"LOOK ASTERN": "VISTA A POPA",
"UP {angle}°": "ARRIBA {angle}°",
"DOWN {angle}°": "ABAJO {angle}°",
@@ -232,7 +232,7 @@
"BRIDGE WINDOW · FWD": "VENTANA DEL PUENTE · AVANTE",
"OPTICAL PLANE · 01": "PLANO ÓPTICO · 01",
"HEADING HOLD · AIS": "BLOQUEO DE PROA · AIS",
"NO HEADING · AIS": "PROA N/D · AIS",
"NO HEADING · AIS": "SIN PROA · AIS",
"VISOR LOCK · ACTIVE": "BLOQUEO DE VISERA · ACTIVO",
"FIRST PERSON": "PRIMERA PERSONA",
"BRIDGE VIEW": "VISTA DEL PUENTE",
@@ -240,7 +240,7 @@
"Cockpit vision style": "Estilo de visión de la cabina",
"Current aircraft heading": "Rumbo actual de la aeronave",
"Look ahead: turn the bridge view back to the bow": "Mirar a proa: girar de nuevo la vista del puente hacia la proa",
"Look ahead (Home or 0)": "Mirar a proa (Home o 0)",
"Look ahead (Home or 0)": "Mirar a proa (Inicio o 0)",
"LOOK AHEAD": "MIRAR A PROA",
"Current vessel heading": "Proa actual del buque",
"GROUND SPEED · KTS": "VEL. SUELO · KN",
@@ -353,7 +353,7 @@
"TRACK ACQUIRED": "RASTREO ADQUIRIDO",
"{label} · COURSE {heading}°": "{label} · RUMBO {heading}°",
"{label} · HEADING {heading}°": "{label} · PROA {heading}°",
"{label} · NO HEADING": "{label} · PROA N/D",
"{label} · NO HEADING": "{label} · SIN PROA",
"{label} · NADIR VIEW": "{label} · VISTA AL NADIR",
"Select flight {title}": "Seleccionar el vuelo {title}",
"CONTEXT STANDBY": "CONTEXTO EN ESPERA",
@@ -502,9 +502,9 @@
"other": "{count} cámaras cargadas · activa CCTV para activarlas"
},
"{count} more cameras outside this view · pan or zoom out": {
"one": "{count} cámara más fuera de esta vista · desplaza el mapa o aléjate",
"many": "{count} de cámaras más fuera de esta vista · desplaza el mapa o aléjate",
"other": "{count} cámaras más fuera de esta vista · desplaza el mapa o aléjate"
"one": "{count} cámara más fuera de esta vista · desplaza o aleja",
"many": "{count} de cámaras más fuera de esta vista · desplaza o aleja",
"other": "{count} cámaras más fuera de esta vista · desplaza o aleja"
},
"No summary available.": "No hay resumen disponible.",
"Celestial ring — available in Normal style": "Anillo celestial — disponible en el estilo Normal",
@@ -694,7 +694,7 @@
"Directory metadata only": "Solo metadatos del directorio",
"Choose a globe marker or use next.": "Elige un marcador del globo o pulsa SIG.",
"TAGS · {tags}": "ETIQUETAS · {tags}",
"Ready — playback starts only from your action": "Preparada: la reproducción solo empieza cuando tú la inicias",
"Ready — playback starts only from your action": "Preparada: la reproducción empieza cuando pulses ESCUCHAR",
"Connecting directly to broadcaster…": "Conectando directamente con la emisora…",
"Buffering broadcaster stream…": "Almacenando en búfer el flujo de la emisora…",
"Playing {station}": "Reproduciendo {station}",
@@ -756,7 +756,7 @@
"{direction} {angle}°": "{direction} {angle}°",
"OFF-NADIR {angle}° · {offset}": "FUERA DEL NADIR {angle}° · {offset}",
"SITES STANDBY": "SITIOS EN ESPERA",
"ENABLE MAPPED INSTALLATIONS FOR SITE PINGS": "ACTIVA INSTALACIONES MAPEADAS PARA PINGS DE SITIOS",
"ENABLE MAPPED INSTALLATIONS FOR SITE PINGS": "ACTIVAR INSTALACIONES CARTOGRAFIADAS PARA PINGS DE SITIOS",
"Release the camera from {name}": "Soltar la cámara de {name}",
"RETURN · {name}": "VOLVER · {name}",
"PREVIOUS SATELLITE": "SATÉLITE ANTERIOR",
@@ -785,7 +785,7 @@
"Hide the two-line element set": "Ocultar el conjunto de elementos de dos líneas",
"Two-line element set": "Conjunto de elementos de dos líneas",
"Return the view to nadir": "Volver la vista al nadir",
"Return to nadir (Home or 0)": "Volver al nadir (Home o 0)",
"Return to nadir (Home or 0)": "Volver al nadir (Inicio o 0)",
"NADIR": "AL NADIR",
"REPLAY ASCENT": "REPETIR ASCENSO",
"Replay the estimated ascent with a following camera": "Reproduce el ascenso estimado con una cámara de seguimiento",
@@ -1773,10 +1773,10 @@
"Weather data by Open-Meteo.com": "Datos meteorológicos de Open-Meteo.com",
"ESC EXIT": "ESC SALIR",
"ARROWS LOOK": "FLECHAS MIRAR",
"HOME RESET": "HOME RESTABLECER",
"HOME RESET": "INICIO RESTABLECER",
"+/− ZOOM": "+/− ACERCAR/ALEJAR",
"ARROWS TILT": "FLECHAS INCLINAR",
"HOME NADIR": "HOME AL NADIR",
"HOME NADIR": "INICIO NADIR",
"SIG": "SIG",
"NEWS": "NOTICIAS",
"LOCAL": "LOCAL",
@@ -2096,7 +2096,7 @@
"{scope} · INSTALLATIONS {coverage}": "{scope} · INSTALACIONES: {coverage}",
"MILITARY FEED OFF · PARTIAL PICTURE": "FUENTE MILITAR DESACTIVADA · PANORAMA PARCIAL",
"FLIGHTS FEED OFF · PARTIAL PICTURE": "FUENTE DE VUELOS DESACTIVADA · PANORAMA PARCIAL",
"NEARBY AIRCRAFT INCOMPLETE · NOT AN ALL-CLEAR": "AERONAVES CERCANAS: DATOS INCOMPLETOS · NO IMPLICA TODO DESPEJADO",
"NEARBY AIRCRAFT INCOMPLETE · NOT AN ALL-CLEAR": "AERONAVES CERCANAS INCOMPLETAS · NO IMPLICA TODO DESPEJADO",
"{source} · LAST {hours} H": "{source} · ÚLTIMAS {hours} H",
"ft": "ft",
"kts": "kn",
@@ -2339,7 +2339,7 @@
"Regional sample · 250 nm radius · cells from visited views over {minutes} min": "Muestra regional · radio de 250 NM · celdas de las vistas visitadas en {minutes} min",
"Retrying automatically at {time} UTC": "Reintento automático a las {time} UTC",
"Retrying automatically after provider cooldown": "Reintento automático tras la pausa del proveedor",
"Updates automatically after pan or zoom and once a minute. The public feed covers a 250-nautical-mile circle around the view centre; zooming out does not make it a worldwide snapshot. Blank cells are outside sampled coverage or have insufficient observations.": "Se actualiza automáticamente al desplazar el mapa o hacer zoom y una vez por minuto. La fuente pública cubre un círculo de 250 millas náuticas alrededor del centro de la vista; alejarte no la convierte en una instantánea mundial. Las celdas vacías están fuera de la cobertura muestreada o no tienen observaciones suficientes.",
"Updates automatically after pan or zoom and once a minute. The public feed covers a 250-nautical-mile circle around the view centre; zooming out does not make it a worldwide snapshot. Blank cells are outside sampled coverage or have insufficient observations.": "Se actualiza automáticamente al desplazar o hacer zoom y una vez por minuto. El feed público cubre un círculo de 250 millas náuticas alrededor del centro de la vista; alejarte no lo convierte en una instantánea mundial. Las celdas vacías están fuera de la cobertura muestreada o no tienen observaciones suficientes.",
"Low-accuracy score = 100 × max(0, bad − 1) / total: one aircraft is discounted, so this is not the raw share. Cells need {aircraft} aircraft over {minutes} minutes. Navigation-integrity anomalies are not a detection of jamming or spoofing.": "Índice de baja precisión = 100 × max(0, malas − 1) / total: se descuenta una aeronave, así que no es la proporción bruta. Cada celda necesita {aircraft} aeronaves en {minutes} minutos. Las anomalías de integridad de navegación no son una detección de interferencias ni de suplantación.",
"SQUAWK {code} · {status}": "SQUAWK {code} · {status}",
"ADS-B PRIORITY · {status}": "ADS-B PRIORIDAD · {status}",
@@ -2450,7 +2450,7 @@
"TomTom key unavailable": "Clave de TomTom no disponible",
"TomTom daily budget reached": "Se alcanzó el límite diario de TomTom",
"Monthly traffic allowance used": "Cupo mensual de tráfico agotado",
"Traffic rate limit reached": "Límite de peticiones de tráfico alcanzado",
"Traffic rate limit reached": "Límite de solicitudes de tráfico alcanzado",
"Traffic service unavailable": "Servicio de tráfico no disponible",
"TomTom upstream unreachable": "Servidor de TomTom inaccesible",
"TomTom flow unavailable": "Flujo de TomTom no disponible",
@@ -3114,7 +3114,7 @@
"Changing provider reconnects voice if it is on.": "Cambiar de proveedor vuelve a conectar la voz si está activada.",
"Gemini voice could not connect. Try again.": "No se pudo conectar la voz de Gemini. Inténtalo de nuevo.",
"Gemini voice requires a same-origin app request": "La voz de Gemini requiere una solicitud del mismo origen que la aplicación",
"Add a Gemini API key in Provider Settings": "Añade una clave de API de Gemini en Ajustes de proveedores",
"Add a Gemini API key in Provider Settings": "Añade una clave de API de Gemini en los ajustes de proveedores",
"The Gemini Live model configuration is invalid": "La configuración del modelo de Gemini Live no es válida",
"Gemini voice is starting too often. Try again shortly.": "La voz de Gemini se está iniciando con demasiada frecuencia. Vuelve a intentarlo en breve.",
"Gemini quota is unavailable. Try again later.": "La cuota de Gemini no está disponible. Inténtalo más tarde.",
@@ -3215,18 +3215,18 @@
"Add the current view, style and layers as the next shot": "Añade la vista, el estilo y las capas actuales como el siguiente plano",
"Replace the selected shot with the current view": "Sustituye el plano seleccionado por la vista actual",
"Click a shot to select it, double-click to rename.": "Haz clic en un plano para seleccionarlo y doble clic para cambiarle el nombre.",
"scene|PLAY": "REPRODUCIR",
"scene|PLAY": "REPROD.",
"Stop playback (Esc)": "Detén la reproducción (Esc)",
"SAVE & SHARE": "GUARDAR Y COMPARTIR",
"Download every scene as a JSON file you can import later": "Descarga todas las escenas como un archivo JSON que puedes importar más tarde",
"Load scenes from an exported JSON file": "Carga escenas desde un archivo JSON exportado",
"Download the timing log of the last play": "Descarga el registro de tiempos de la última reproducción",
"Play from this scene through the rest of the project": "Reproduce desde esta escena hasta el final del proyecto",
"Play from this scene through the rest of the project": "Reproduce desde esta escena hasta el resto del proyecto",
"Play the shot after the selected one, moving on to the next scene when needed": "Reproduce el plano posterior al seleccionado y pasa a la siguiente escena si hace falta",
"Edit anchors, data packs and shot details": "Edita anclas, paquetes de datos y detalles de los planos",
"Download this scene as JSON or an asset bundle": "Descarga esta escena como JSON o como paquete de recursos",
"Left panels": "Paneles de la izquierda",
"Right panels": "Paneles de la derecha",
"Left panels": "Paneles izquierdos",
"Right panels": "Paneles derechos",
"UTILITIES": "UTILIDADES",
"Exit cockpit to fly to a drawing": "Sal de la cabina para volar a un dibujo",
"Directions, drawing and your own files": "Indicaciones, dibujo y tus propios archivos",
@@ -3393,7 +3393,7 @@
"Date unknown": "Fecha desconocida",
"Position: centre of the protected area": "Posición: centro de la zona protegida",
"Position approximate (within about {km} km)": "Posición aproximada (margen de unos {km} km)",
"Position surveyed (within about {m} m)": "Posición medida (margen de unos {m} m)",
"Position surveyed (within about {m} m)": "Posición levantada (margen de unos {m} m)",
"Vessel: {value}": "Embarcación: {value}",
"What happened: {value}": "Qué ocurrió: {value}",
"Depth: {m} m ({quality})": "Profundidad: {m} m ({quality})",
@@ -3402,8 +3402,8 @@
"Cargo or finds: {value}": "Carga o hallazgos: {value}",
"Protected wreck": "Pecio protegido",
"Source: {value}": "Fuente: {value}",
"Click the card to open EMODnet ↗": "Haz clic en la tarjeta para abrir EMODnet ↗",
"Click the card to open the source ↗": "Haz clic en la tarjeta para abrir la fuente ↗",
"Click the card to open EMODnet ↗": "Haz clic en la ficha para abrir EMODnet ↗",
"Click the card to open the source ↗": "Haz clic en la ficha para abrir la fuente ↗",
"Wikidata unavailable; showing EMODnet only": "Wikidata no disponible; solo se muestra EMODnet",
"EMODnet unavailable; showing Wikidata only": "EMODnet no disponible; solo se muestra Wikidata",
"Shipwrecks unavailable": "Naufragios no disponibles",
@@ -3417,8 +3417,8 @@
"Shipwreck": "Naufragio",
"Fatalities: {count}": "Fallecidos: {count}",
"Survivors: {count}": "Supervivientes: {count}",
"Click the card to open the article ↗": "Haz clic en la tarjeta para abrir el artículo ↗",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Los pecios históricos europeos proceden de EMODnet Human Activities; los naufragios notables de todo el mundo, de Wikidata. El color indica cuándo se hundió el barco; el marcador crece con el número de muertos. Haz clic en un marcador para ver su tarjeta.",
"Click the card to open the article ↗": "Haz clic en la ficha para abrir el artículo ↗",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Los pecios históricos europeos proceden de EMODnet Human Activities; los naufragios notables de todo el mundo, de Wikidata. El color indica cuándo se hundió el barco; el marcador crece con el número de muertos. Haz clic en un marcador para ver su ficha.",
"All years": "Todos los años",
"Period": "Periodo",
"From": "Desde",
@@ -3447,7 +3447,7 @@
"{label} {detail} — click to retry": "{label} {detail} — haz clic para reintentar",
"Wildlife sightings source unavailable": "Fuente de avistamientos no disponible",
"Show only community-confirmed (research-grade) sightings": "Mostrar solo observaciones confirmadas por la comunidad (grado de investigación)",
"No recent sightings in view": "No hay avistamientos recientes a la vista",
"No recent sightings in view": "No hay avistamientos recientes en la vista",
"Acartia feed unavailable": "Fuente Acartia no disponible",
"Show all taxa": "Mostrar todos los grupos taxonómicos",
"BIRDS": "AVES",
@@ -3466,33 +3466,33 @@
"Exit cockpit to fly to a sighting": "Sal de la cabina para volar a un avistamiento",
"Exit cockpit to fly to a sighting area": "Sal de la cabina para volar a un área de avistamiento",
"Wildlife": "Vida silvestre",
"Zoom in for local sightings": "Acércate para ver avistamientos locales",
"Zoom in for local sightings": "Acerca para ver avistamientos locales",
"Loading photo...": "Cargando foto…",
"Photo unavailable": "Foto no disponible",
"Location obscured to a 0.2° cell": "Ubicación oculta en una celda de 0,2°",
"Photo of {name}": "Foto de {name}",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": "Avistamientos a la vista: {count}",
"{count} sightings in view": "{count} avistamientos en la vista",
"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.",
"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 observada 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",
"Open {name} on Acartia": "Abrir {name} en Acartia",
"iNaturalist": "iNaturalist",
"iNaturalist contributors · licences per record": "Colaboradores de iNaturalist · licencias por registro",
"iNaturalist contributors · Acartia {count} (Salish Sea) · licences per record": "Colaboradores de iNaturalist · Acartia {count} (mar de Salish) · licencias por registro",
"{days}d": "{days} d",
"{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": "Antigüedad máxima: {days} d; se conservan los marcadores sin fecha",
"Maximum event age: {days}d; undated markers are kept": "Antigüedad máxima: {days} días; se conservan los marcadores sin fecha",
"event age filter|All": "Todos",
"Event age": "Antigüedad",
"Event age: {days}d · undated kept": "Antigüedad: {days} d · se conservan los marcadores sin fecha",
"Event age: {days}d · undated kept": "Antigüedad: {days} días · 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",
"Press OPEN to load the photo": "Pulse ABRIR para cargar la foto",
"Captured {month}": "Capturada en {month}",
"this spot": "este punto",
"the view centre": "el centro de la vista",
@@ -3537,40 +3537,38 @@
"Zoom out (−)": "Alejar (−)",
"ORBITAL VIEW · OFF-NADIR": "VISTA ORBITAL · FUERA DEL NADIR",
"World time": "Hora mundial",
"Back to live": "Volver al modo en vivo",
"Live": "En vivo",
"Back to live": "Volver al directo",
"Live": "En directo",
"1 h/s": "1 h/s",
"1 d/s": "1 d/s",
"1 wk/s": "1 sem/s",
"Play speed": "Velocidad de reproducción",
"Scroll or pinch to zoom, drag to move along · double-click to see it all": "Usa la rueda o pellizca para hacer zoom, arrastra para desplazarte · doble clic para verlo todo",
"Scroll or pinch to zoom, drag to move along · double-click to see it all": "Desplaza o pellizca para ampliar, arrastra para moverte · doble clic para verlo todo",
"1 min/s": "1 min/s",
"Rail": "Ferrocarril",
"Urban rail": "Urbano",
"Local rail": "Suburbano",
"Local rail": "Cercanías",
"Regional rail": "Regional",
"National rail": "Larga distancia",
"International rail": "Internacional",
"Hide {category}": "Ocultar {category}",
"Show {category}": "Mostrar {category}",
"Live GPS": "GPS en vivo",
"Estimated from station reports": "Posición estimada a partir de avisos de estaciones",
"Estimated from station reports": "Estimada a partir de avisos de estaciones",
"To {headsign}": "Hacia {headsign}",
"Next {stop} {time} {relative}": "Próxima parada {stop} {time} {relative}",
"Prev {stop} {time} {relative}": "Parada anterior {stop} {time} {relative}",
"(+{minutes} min late)": "(+{minutes} min de retraso)",
"Exit cockpit to fly to a train": "Sal de la cabina para volar a un tren",
"Photo © {photographer}": "Foto © {photographer}",
"Photo: {photographer} · {licence}": "Foto: {photographer} · {licence}",
"Photo · {licence}": "Foto · {licence}",
"The shared flight isn't reporting now — showing where it was last seen": "El vuelo compartido no está transmitiendo ahora; se muestra dónde se vio por última vez",
"The shared flight isn't reporting now — showing where it was at {time}": "El vuelo compartido no está transmitiendo ahora; se muestra dónde estaba ({time})",
"The shared flight isn't reporting now — showing where it was at {time}": "El vuelo compartido no está transmitiendo ahora; se muestra dónde estaba a las {time}",
"The shared military flight isn't reporting now — showing where it was last seen": "El vuelo militar compartido no está transmitiendo ahora; se muestra dónde se vio por última vez",
"The shared military flight isn't reporting now — showing where it was at {time}": "El vuelo militar compartido no está transmitiendo ahora; se muestra dónde estaba ({time})",
"The shared military flight isn't reporting now — showing where it was at {time}": "El vuelo militar compartido no está transmitiendo ahora; se muestra dónde estaba a las {time}",
"The shared vessel isn't reporting now — showing where it was last seen": "El buque compartido no está transmitiendo ahora; se muestra dónde se vio por última vez",
"The shared vessel isn't reporting now — showing where it was at {time}": "El buque compartido no está transmitiendo ahora; se muestra dónde estaba ({time})",
"The shared vessel isn't reporting now — showing where it was at {time}": "El buque compartido no está transmitiendo ahora; se muestra dónde estaba a las {time}",
"The shared satellite isn't reporting now — showing where it was last seen": "El satélite compartido no está transmitiendo ahora; se muestra dónde se vio por última vez",
"The shared satellite isn't reporting now — showing where it was at {time}": "El satélite compartido no está transmitiendo ahora; se muestra dónde estaba ({time})",
"The shared satellite isn't reporting now — showing where it was at {time}": "El satélite compartido no está transmitiendo ahora; se muestra dónde estaba a las {time}",
"ICAO: {id}": "ICAO: {id}",
"NORAD: {id}": "NORAD: {id}",
"MMSI: {id}": "MMSI: {id}",
@@ -3686,7 +3684,7 @@
"Resolution": "Resolución",
"Flown": "Vuelo",
"Updates": "Actualización",
"Ortho quality": "Calidad de la ortofoto",
"Ortho quality": "Calidad del orto",
"Below 250 m": "Menos de 250 m",
"250–500 m": "250–500 m",
"500–1,000 m": "500–1000 m",
+106 -108
View File
@@ -1,32 +1,32 @@
{
"News feed unavailable": "Fil d'actualités indisponible",
"Developing: {name}": "En cours : {name}",
"News feed unavailable": "Fil d’actualités indisponible",
"Developing: {name}": "En cours : {name}",
"{count} events today": {
"one": "{count} événement aujourd'hui",
"many": "{count} d'événements aujourd'hui",
"other": "{count} événements aujourd'hui"
"one": "{count} événement aujourd’hui",
"many": "{count} événements aujourd’hui",
"other": "{count} événements aujourd’hui"
},
"{count} reports today": {
"one": "{count} article aujourd'hui",
"many": "{count} d'articles aujourd'hui",
"other": "{count} articles aujourd'hui"
"one": "{count} article aujourd’hui",
"many": "{count} articles aujourd’hui",
"other": "{count} articles aujourd’hui"
},
"Disputed: {name}": "Zone contestée : {name}",
"Disputed: {name}": "Contesté : {name}",
"Video": "Vidéo",
"Photos": "Photos",
"Claimed by {who}": "Selon {who}",
"Claimed": "Affirmation",
"Not yet confirmed by others": "Pas encore confirmé par d'autres",
"Reported by {side} sources": "Rapporté par des sources : {side}",
"Reported by {sides}": "Rapporté par des sources : {sides}",
"{side} sources": "{side}",
"Not yet confirmed by others": "Pas encore confirmé par d’autres",
"Reported by {side} sources": "Rapporté par des sources {side}",
"Reported by {sides}": "Rapporté par {sides}",
"{side} sources": "sources {side}",
"Official": "Officiel",
"Reported by both sides": "Rapporté par les deux camps",
"Independently reported": "Rapporté de façon indépendante",
"Single source": "Source unique",
"International": "Internationale",
"Independent": "Indépendante",
"Unattributed": "Non attribuée",
"Unattributed": "Non attribué",
"Outside party": "Partie extérieure",
"Ukraine–Russia": "Ukraine–Russie",
"Israel–Gaza–Lebanon": "Israël–Gaza–Liban",
@@ -48,19 +48,19 @@
"Minor incident": "Incident mineur",
"Significant": "Important",
"Major": "Majeur",
"age|now": "à l'instant",
"age|now": "maintenant",
"{minutes} min": "{minutes} min",
"{hours} h": "{hours} h",
"{count} sources": {
"one": "{count} source",
"many": "{count} de sources",
"many": "{count} sources",
"other": "{count} sources"
},
"News feed not configured on this server": "Fil d'actualités non configuré sur ce serveur",
"Malformed news hotspots response": "Réponse des foyers d'actualité mal formée",
"Malformed news feed response": "Réponse du fil d'actualités mal formée",
"News feed not configured on this server": "Fil d’actualités non configuré sur ce serveur",
"Malformed news hotspots response": "Réponse des foyers d’actualité mal formée",
"Malformed news feed response": "Réponse du fil d’actualités mal formée",
"{place} (placed by model)": "{place} (placé par le modèle)",
"English headline by the model": "Titre traduit en anglais par le modèle",
"English headline by the model": "Titre anglais du modèle",
"Not placed on the map": "Non placé sur la carte",
"Close": "Fermer",
"Any severity": "Toute gravité",
@@ -68,26 +68,26 @@
"{severity} only": "{severity} uniquement",
"All categories": "Toutes les catégories",
"Reading the news…": "Lecture des actualités…",
"News, last {hours} hours": "Actualités sur {hours} h",
"Last {hours} h": "Sur {hours} h",
"News, last {hours} hours": "Actualités des dernières {hours} heures",
"Last {hours} h": "Dernières {hours} h",
"Time window": "Période",
"Conflicts": "Conflits",
"Local": "Local",
"News sections": "Rubriques d'actualité",
"Zoom in to see local news": "Zoomez pour voir l'actualité locale",
"News sections": "Rubriques d’actualité",
"Zoom in to see local news": "Zoomez pour voir l’actualité locale",
"Hotspots": "Foyers",
"Severity": "Gravité",
"Category": "Catégorie",
"Nothing reported near here in the last {hours} h": "Rien de signalé près d'ici en {hours} h",
"Nothing reported near here in the last {hours} h": "Rien de signalé près d’ici ces dernières {hours} h",
"Latest news": "Dernières actualités",
"No news matches these filters in the last {hours} h": "Aucune actualité ne correspond à ces filtres sur {hours} h",
"No news matches these filters in the last {hours} h": "Aucune actualité des dernières {hours} h ne correspond à ces filtres",
"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",
"Solid rim: reported by both sides or independently. Dashed rim: a claim. Thin rim: one source, or one side’s own press.": "Bord plein : rapporté par les deux camps ou de façon indépendante. Bord en tirets : une affirmation. Bord fin : une source, ou la presse d'un camp.",
"Larger: more sources · fainter: older. Picture card: a major or fast-growing event with two or more sources.": "Plus grand : plus de sources · plus pâle : plus ancien. Fiche illustrée : un événement majeur ou en forte croissance, avec au moins deux sources.",
"Side labels describe the source, not whether a report is true.": "Les étiquettes de camp décrivent la source, pas la véracité de l'article.",
"Exit cockpit to fly to a news report": "Quittez le cockpit pour rejoindre un article",
"Colour: severity · icon: type of event": "Couleur : gravité · icône : type d’événement",
"Solid rim: reported by both sides or independently. Dashed rim: a claim. Thin rim: one source, or one side’s own press.": "Bord plein : rapporté par les deux camps ou de façon indépendante. Bord en tirets : une affirmation. Bord fin : une source, ou la presse d’un camp.",
"Larger: more sources · fainter: older. Picture card: a major or fast-growing event with two or more sources.": "Plus grand : plus de sources · plus pâle : plus ancien. Carte illustrée : un événement majeur ou en forte croissance, avec au moins deux sources.",
"Side labels describe the source, not whether a report is true.": "Les étiquettes de camp décrivent la source, pas la véracité de l’article.",
"Exit cockpit to fly to a news report": "Quittez le cockpit pour aller à un article",
"News items in the feed": "Articles dans le fil",
"Pick a shape, then click the map.": "Choisissez une forme, puis cliquez sur la carte.",
"Enter to place the pin, Esc to cancel.": "Enter pour poser l'épingle, Esc pour annuler.",
@@ -951,7 +951,7 @@
"Hide the two-line element set": "Masquer le jeu d'éléments à deux lignes",
"Two-line element set": "Jeu d'éléments à deux lignes",
"Return the view to nadir": "Ramener la vue au nadir",
"Return to nadir (Home or 0)": "Revenir au nadir (Home ou 0)",
"Return to nadir (Home or 0)": "Retour au nadir (Home ou 0)",
"NADIR": "AU NADIR",
"Selected Space Mission": "Mission spatiale sélectionnée",
"SELECTED SPACE MISSION": "MISSION SPATIALE SÉLECTIONNÉE",
@@ -1754,9 +1754,9 @@
"other": "{count} caméras chargées · activer CCTV pour les utiliser"
},
"{count} more cameras outside this view · pan or zoom out": {
"one": "{count} autre caméra hors de cette vue · déplacez la carte ou dézoomez",
"many": "{count} d'autres caméras hors de cette vue · déplacez la carte ou dézoomez",
"other": "{count} autres caméras hors de cette vue · déplacez la carte ou dézoomez"
"one": "{count} autre caméra hors de cette vue · déplacer ou dézoomer",
"many": "{count} d'autres caméras hors de cette vue · déplacer ou dézoomer",
"other": "{count} autres caméras hors de cette vue · déplacer ou dézoomer"
},
"Enable CCTV to load camera intersections": "Activez CCTV pour charger les carrefours équipés de caméras",
"Enable CCTV to start camera-linked intelligence summaries.": "Activez CCTV pour lancer les résumés de renseignement liés aux caméras.",
@@ -1782,7 +1782,7 @@
"Cycle briefing pages automatically every 9 seconds (Signals → News → Local). Pauses while you hover or focus the panel. Live signal data refreshes continuously either way.": "Faire défiler automatiquement les pages du briefing toutes les 9 secondes (Signaux → Actus → Local). Pause pendant le survol ou quand le panneau a le focus. Les signaux en direct s'actualisent en continu dans les deux cas.",
"ACQUIRING REGIONAL NEWS": "ACQUISITION DES ACTUS RÉGIONALES",
"REGIONAL NEWS UNAVAILABLE": "ACTUS RÉGIONALES INDISPONIBLES",
"CACHED · REFRESH FAILED": "EN CACHE · ÉCHEC DE L'ACTUALISATION",
"CACHED · REFRESH FAILED": "EN CACHE · ÉCHEC DE L’ACTUALISATION",
"RESOLVING REGION": "IDENTIFICATION DE LA RÉGION",
"REGION UNAVAILABLE": "RÉGION INDISPONIBLE",
"NO NEWS IN THE LAST {hours} H": "AUCUNE ACTU CES {hours} DERNIÈRES H",
@@ -1794,9 +1794,9 @@
"REGIONAL NEWS": "ACTUS RÉGIONALES",
"{label} · {radius} AIR/SEA WINDOW": "{label} · AIR/MER {radius}",
"{scope} · INSTALLATIONS {coverage}": "{scope} · INSTALLATIONS : {coverage}",
"MILITARY FEED OFF · PARTIAL PICTURE": "FLUX MILITAIRE DÉSACTIVÉ · SITUATION PARTIELLE",
"FLIGHTS FEED OFF · PARTIAL PICTURE": "FLUX DES VOLS DÉSACTIVÉ · SITUATION PARTIELLE",
"NEARBY AIRCRAFT INCOMPLETE · NOT AN ALL-CLEAR": "AÉRONEFS PROCHES : DONNÉES INCOMPLÈTES · PAS UN FEU VERT",
"MILITARY FEED OFF · PARTIAL PICTURE": "FLUX MILITAIRE COUPÉ · SITUATION PARTIELLE",
"FLIGHTS FEED OFF · PARTIAL PICTURE": "FLUX DES VOLS COUPÉ · SITUATION PARTIELLE",
"NEARBY AIRCRAFT INCOMPLETE · NOT AN ALL-CLEAR": "AÉRONEFS PROCHES INCOMPLETS · PAS UN FEU VERT",
"CONTEXT STANDBY": "CONTEXTE EN VEILLE",
"ENABLE GLOBAL CONTEXT FOR PROXIMITY PINGS": "ACTIVER LE CONTEXTE GLOBAL POUR LES PINGS DE PROXIMITÉ",
"CONTACT LOST · LAST KNOWN READOUT · NOT AN ALL-CLEAR": "CONTACT PERDU · DERNIER RELEVÉ CONNU · PAS UN FEU VERT",
@@ -1832,7 +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": "{range} NM",
"{range} NM": "{range} MN",
"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",
@@ -2867,7 +2867,7 @@
"Next cockpit vision style": "Style de vision suivant du cockpit",
"Next vision style": "Style de vision suivant",
"Current aircraft heading": "Cap actuel de l'aéronef",
"Look ahead: turn the bridge view back to the bow": "Vue avant : ramener la vue passerelle vers l'étrave",
"Look ahead: turn the bridge view back to the bow": "Vue avant : ramener la vue passerelle vers l'étrave",
"Look ahead (Home or 0)": "Vue avant (Home ou 0)",
"LOOK AHEAD": "VUE AVANT",
"Current vessel heading": "Cap actuel du navire",
@@ -2880,7 +2880,7 @@
"Contact cockpit summary": "Résumé cockpit du contact",
"CONTACT": "CONTACT",
"CONTACTS · 250 KM": "CONTACTS · 250 KM",
"RINGS {spacing} NM": "CERCLES {spacing} NM",
"RINGS {spacing} NM": "CERCLES {spacing} MN",
"{count} CONTACTS": {
"one": "{count} CONTACT",
"many": "{count} DE CONTACTS",
@@ -3195,7 +3195,7 @@
"Add the current view, style and layers as the next shot": "Ajouter la vue, le style et les couches actuels comme plan suivant",
"Replace the selected shot with the current view": "Remplacer le plan sélectionné par la vue actuelle",
"Click a shot to select it, double-click to rename.": "Cliquez sur un plan pour le sélectionner, double-cliquez pour le renommer.",
"Play from this scene through the rest of the project": "Lire à partir de cette scène jusqu'à la fin du projet",
"Play from this scene through the rest of the project": "Lire de cette scène jusqu'à la fin du projet",
"Stop playback (Esc)": "Arrêter la lecture (Esc)",
"Play the shot after the selected one, moving on to the next scene when needed": "Lire le plan qui suit la sélection, en passant à la scène suivante si besoin",
"SAVE & SHARE": "ENREGISTRER ET PARTAGER",
@@ -3372,40 +3372,40 @@
"Too many chat requests. Try again in {seconds} s.": "Trop de requêtes de discussion. Réessayez dans {seconds} s.",
"The provider refused access for this key. Check the key's project, region or permissions.": "Le fournisseur a refusé l'accès pour cette clé. Vérifiez le projet, la région ou les autorisations de la clé.",
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "Le compte du fournisseur n'a plus de crédit. Vérifiez la facturation ou choisissez un autre fournisseur d'IA dans les paramètres.",
"Last 24 h of news. Each report links to its publisher; places and categories are read from the text and data automatically and can be wrong. Side labels, where shown, describe the source, not whether a report is true.": "Dernières 24 h d'actualité. Chaque article renvoie à son éditeur ; lieux et catégories sont lus automatiquement dans le texte et les données et peuvent être faux. Les étiquettes de camp, là où elles figurent, décrivent la source, pas la véracité de l'article.",
"Last 24 h of news. Each report links to its publisher; places and categories are read from the text and data automatically and can be wrong. Side labels, where shown, describe the source, not whether a report is true.": "Dernières 24 h d’actualité. Chaque article renvoie à son éditeur ; lieux et catégories sont lus automatiquement dans le texte et les données et peuvent être faux. Les étiquettes de camp, là où elles figurent, décrivent la source, pas la véracité de l’article.",
"Economy": "Économie",
"News from {area}": "Actualités : {area}",
"News from {area}": "Actualités : {area}",
"Within {km} km of the view centre": "À moins de {km} km du centre de la vue",
"{region}, {country}": "{region}, {country}",
"via {source}": "via {source}",
"Local in the News layer: the day’s news from the outlets of the country in view": "Local dans la couche Actualités : l'actualité du jour des médias du pays affiché",
"Local in the News layer: the day’s news from the outlets of the country in view": "Local dans la couche Actualités : l’actualité du jour des médias du pays affiché",
"{provider} refused its key": "{provider} a refusé sa clé",
"Check {envVars} in Provider Settings": "Vérifiez {envVars} dans les paramètres des fournisseurs",
"{provider} key required": "Clé {provider} requise",
"{provider} limit reached for today": "{provider} a atteint sa limite du jour",
"News resumes tomorrow, or with a paid plan": "Les actualités reprennent demain, ou avec une offre payante",
"News key required": "Clé d'actualités requise",
"News key required": "Clé d’actualités requise",
"Precise conflict points in the News layer, beside GDELT’s articles": "Points de conflit précis dans la couche Actualités, à côté des articles de GDELT",
"Worldwide": "Monde entier",
"Shipwrecks": "Épaves",
"Before 1900": "Avant 1900",
"After 1945": "Après 1945",
"Date unknown": "Date inconnue",
"Position: centre of the protected area": "Position : centre de la zone protégée",
"Position: centre of the protected area": "Position : centre de la zone protégée",
"Position approximate (within about {km} km)": "Position approximative (à environ {km} km près)",
"Position surveyed (within about {m} m)": "Position relevée (à environ {m} m près)",
"Vessel: {value}": "Navire : {value}",
"What happened: {value}": "Ce qui s'est passé : {value}",
"Depth: {m} m ({quality})": "Profondeur : {m} m ({quality})",
"Depth: {m} m": "Profondeur : {m} m",
"Notes: {value}": "Notes : {value}",
"Cargo or finds: {value}": "Cargaison ou découvertes : {value}",
"Vessel: {value}": "Navire : {value}",
"What happened: {value}": "Ce qui s'est passé : {value}",
"Depth: {m} m ({quality})": "Profondeur : {m} m ({quality})",
"Depth: {m} m": "Profondeur : {m} m",
"Notes: {value}": "Notes : {value}",
"Cargo or finds: {value}": "Cargaison ou découvertes : {value}",
"Protected wreck": "Épave protégée",
"Source: {value}": "Source : {value}",
"Source: {value}": "Source : {value}",
"Click the card to open EMODnet ↗": "Cliquez sur la fiche pour ouvrir EMODnet ↗",
"Click the card to open the source ↗": "Cliquez sur la fiche pour ouvrir la source ↗",
"Wikidata unavailable; showing EMODnet only": "Wikidata indisponible ; seul EMODnet est affiché",
"EMODnet unavailable; showing Wikidata only": "EMODnet indisponible ; seul Wikidata est affiché",
"Wikidata unavailable; showing EMODnet only": "Wikidata indisponible ; seul EMODnet est affiché",
"EMODnet unavailable; showing Wikidata only": "EMODnet indisponible ; seul Wikidata est affiché",
"Shipwrecks unavailable": "Épaves indisponibles",
"EMODnet (European waters) · Wikidata (worldwide)": "EMODnet (eaux européennes) · Wikidata (monde)",
"{count} wrecks": {
@@ -3415,14 +3415,14 @@
},
"1900–1945": "1900–1945",
"Shipwreck": "Naufrage",
"Fatalities: {count}": "Morts : {count}",
"Survivors: {count}": "Survivants : {count}",
"Fatalities: {count}": "Morts : {count}",
"Survivors: {count}": "Survivants : {count}",
"Click the card to open the article ↗": "Cliquez sur la fiche pour ouvrir l'article ↗",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Les épaves patrimoniales européennes viennent d'EMODnet Human Activities ; les épaves notables du monde entier viennent de Wikidata. La couleur indique quand le navire a coulé ; le marqueur grossit avec le nombre de morts. Cliquez sur un marqueur pour voir sa fiche.",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Les épaves patrimoniales européennes viennent d'EMODnet Human Activities ; les épaves notables du monde entier viennent de Wikidata. La couleur indique quand le navire a coulé ; le repère grossit avec le nombre de morts. Cliquez sur un repère pour voir sa fiche.",
"All years": "Toutes les années",
"Period": "Période",
"From": "De",
"To": "À",
"From": "Du",
"To": "Au",
"First year of the shipwreck period": "Première année de la période des naufrages",
"Last year of the shipwreck period": "Dernière année de la période des naufrages",
"SIGHTING": "OBSERVATION",
@@ -3470,10 +3470,10 @@
"Loading photo...": "Chargement de la photo…",
"Photo unavailable": "Photo indisponible",
"Location obscured to a 0.2° cell": "Localisation masquée dans une cellule de 0,2°",
"Photo of {name}": "Photo : {name}",
"Photo of {name}": "Photo de {name}",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": "Observations dans la vue : {count}",
"{count} sightings in view": "{count} observations dans la vue",
"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",
@@ -3496,17 +3496,17 @@
"Captured {month}": "Prise en {month}",
"this spot": "cet endroit",
"the view centre": "le centre de la vue",
"{source} at {place}": "{source} : {place}",
"Street level at {place}": "Niveau rue : {place}",
"{source} at {place}": "{source} à {place}",
"Street level at {place}": "Niveau rue à {place}",
"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",
"Street View is ready: press {open} to load it": "Street View est prêt : appuyez sur {open} pour le charger",
"{source} cannot open this link's street-level photo without a working key": "{source} ne peut pas ouvrir la photo au niveau de la rue de ce lien sans clé valide",
"Street-level photos are hidden in the current view. Leave it first.": "Les photos au niveau de la rue sont masquées dans la vue actuelle. Quittez-la d'abord.",
"Street-level photos are hidden in the current view. Leave it first.": "Les photos au niveau de la rue sont masquées dans la vue actuelle. Quittez-la d’abord.",
"STATIONS": "STATIONS",
"Crewed stations and the craft that visit them": "Stations habitées et les vaisseaux qui s'y rendent",
"IMAGING": "IMAGERIE",
"Earth observation: optical, radar, weather and science": "Observation de la Terre : optique, radar, météo et science",
"Earth observation: optical, radar, weather and science": "Observation de la Terre : optique, radar, météo et science",
"TELECOM": "TÉLÉCOM",
"Communications and navigation satellites, Starlink excepted": "Satellites de communication et de navigation, hors Starlink",
"STARLINK": "STARLINK",
@@ -3515,9 +3515,9 @@
"Space science, technology demonstration and other payloads": "Sciences spatiales, démonstrations technologiques et autres charges utiles",
"DEBRIS": "DÉBRIS",
"Rocket bodies, fragments and dead satellites": "Étages de fusée, fragments et satellites hors service",
"Classes may lag: SATCAT is a cached copy": "Classes peut-être pas à jour : SATCAT est une copie en cache",
"Classes may lag: SATCAT unavailable": "Classes peut-être pas à jour : SATCAT indisponible",
"CelesTrak SATCAT gives each object's type and status. Without a current copy the classes rest on the UCS database, names and CelesTrak groups, so a satellite that has died since may still show as working.": "SATCAT de CelesTrak donne le type et l'état de chaque objet. Sans copie à jour, les classes reposent sur la base UCS, les noms et les groupes CelesTrak : un satellite hors service depuis peut encore apparaître comme actif.",
"Classes may lag: SATCAT is a cached copy": "Classes peut-être en retard : SATCAT est une copie en cache",
"Classes may lag: SATCAT unavailable": "Classes peut-être en retard : SATCAT indisponible",
"CelesTrak SATCAT gives each object's type and status. Without a current copy the classes rest on the UCS database, names and CelesTrak groups, so a satellite that has died since may still show as working.": "SATCAT de CelesTrak donne le type et l'état de chaque objet. Sans copie à jour, les classes reposent sur la base UCS, les noms et les groupes CelesTrak : un satellite hors service depuis peut encore apparaître comme actif.",
"STARLINK ···": "STARLINK ···",
"STARLINK ✕": "STARLINK ✕",
"SENSOR SWATH {value} KM": "FAUCHÉE CAPTEUR {value} KM",
@@ -3531,7 +3531,7 @@
"AFT": "ARRIÈRE",
"RIGHT": "DROITE",
"LEFT": "GAUCHE",
"TRACK UP": "ROUTE EN HAUT",
"TRACK UP": "TRACE EN HAUT",
"COURSE HOLD · AIS": "VERROU ROUTE · AIS",
"Zoom in (+)": "Zoomer (+)",
"Zoom out (−)": "Dézoomer (−)",
@@ -3543,39 +3543,37 @@
"1 d/s": "1 j/s",
"1 wk/s": "1 sem/s",
"Play speed": "Vitesse de lecture",
"Scroll or pinch to zoom, drag to move along · double-click to see it all": "Faites défiler ou pincez pour zoomer, glissez pour vous déplacer · double-cliquez pour tout voir",
"Scroll or pinch to zoom, drag to move along · double-click to see it all": "Faites défiler ou pincez pour zoomer, glissez pour vous déplacer · double-clic pour tout voir",
"1 min/s": "1 min/s",
"Rail": "Trains",
"Urban rail": "Urbain",
"Local rail": "Périurbain",
"Local rail": "Trains locaux",
"Regional rail": "Régional",
"National rail": "Grandes lignes",
"International rail": "International",
"Hide {category}": "Masquer la catégorie {category}",
"Show {category}": "Afficher la catégorie {category}",
"Hide {category}": "Masquer {category}",
"Show {category}": "Afficher {category}",
"Live GPS": "GPS en direct",
"Estimated from station reports": "Position estimée d'après les passages en gare",
"Estimated from station reports": "Estimée d’après les signalements en gare",
"To {headsign}": "Vers {headsign}",
"Next {stop} {time} {relative}": "Prochain arrêt {stop} {time} {relative}",
"Prev {stop} {time} {relative}": "Arrêt précédent {stop} {time} {relative}",
"(+{minutes} min late)": "(+{minutes} min de retard)",
"Exit cockpit to fly to a train": "Quittez le cockpit pour rejoindre un train",
"Photo © {photographer}": "Photo © {photographer}",
"Photo: {photographer} · {licence}": "Photo : {photographer} · {licence}",
"Photo · {licence}": "Photo · {licence}",
"The shared flight isn't reporting now — showing where it was last seen": "Le vol partagé n'émet pas actuellement — affichage de sa dernière position connue",
"The shared flight isn't reporting now — showing where it was at {time}": "Le vol partagé n'émet pas actuellement — affichage de sa position ({time})",
"The shared flight isn't reporting now — showing where it was at {time}": "Le vol partagé n'émet pas actuellement — affichage de sa position à {time}",
"The shared military flight isn't reporting now — showing where it was last seen": "Le vol militaire partagé n'émet pas actuellement — affichage de sa dernière position connue",
"The shared military flight isn't reporting now — showing where it was at {time}": "Le vol militaire partagé n'émet pas actuellement — affichage de sa position ({time})",
"The shared military flight isn't reporting now — showing where it was at {time}": "Le vol militaire partagé n'émet pas actuellement — affichage de sa position à {time}",
"The shared vessel isn't reporting now — showing where it was last seen": "Le navire partagé n'émet pas actuellement — affichage de sa dernière position connue",
"The shared vessel isn't reporting now — showing where it was at {time}": "Le navire partagé n'émet pas actuellement — affichage de sa position ({time})",
"The shared vessel isn't reporting now — showing where it was at {time}": "Le navire partagé n'émet pas actuellement — affichage de sa position à {time}",
"The shared satellite isn't reporting now — showing where it was last seen": "Le satellite partagé n'émet pas actuellement — affichage de sa dernière position connue",
"The shared satellite isn't reporting now — showing where it was at {time}": "Le satellite partagé n'émet pas actuellement — affichage de sa position ({time})",
"ICAO: {id}": "OACI : {id}",
"NORAD: {id}": "NORAD : {id}",
"MMSI: {id}": "MMSI : {id}",
"Vessel UUID: {id}": "UUID du navire : {id}",
"Latitude must be within ±90° and longitude within ±180°": "La latitude doit être comprise entre −90° et 90°, et la longitude entre −180° et 180°",
"The shared satellite isn't reporting now — showing where it was at {time}": "Le satellite partagé n'émet pas actuellement — affichage de sa position à {time}",
"ICAO: {id}": "ICAO : {id}",
"NORAD: {id}": "NORAD : {id}",
"MMSI: {id}": "MMSI : {id}",
"Vessel UUID: {id}": "UUID du navire : {id}",
"Latitude must be within ±90° and longitude within ±180°": "La latitude doit être comprise dans ±90° et la longitude dans ±180°",
"Street Level is unavailable": "Niveau rue n'est pas disponible",
"No street-level imagery source is configured": "Aucune source d'images au niveau de la rue n'est configurée",
"Street Level could not be enabled": "Impossible d'activer Niveau rue",
@@ -3589,22 +3587,22 @@
"{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}",
"{reason} (filters on: {filters})": "{reason} (filtres actifs : {filters})",
"{reason} (filters on: {filters})": "{reason} (filtres actifs : {filters})",
"tiles unavailable": "tuiles indisponibles",
"zoom in to load this layer": "zoomez pour charger cette couche",
"Zoom to layer": "Zoomer sur la couche",
"Zoom to layer · fly to where this layer has data": "Zoomer sur la couche · rejoindre la zone où cette couche a des données",
"Remove from Data Layers": "Retirer du panneau Couches",
"Remove from Data Layers · add it again from the library": "Retirer du panneau Couches · ajoutez-la à nouveau depuis la bibliothèque",
"Zoom to layer · fly to where this layer has data": "Zoomer sur la couche · voler là où cette couche a des données",
"Remove from Data Layers": "Retirer des Couches de données",
"Remove from Data Layers · add it again from the library": "Retirer des Couches de données · ajoutez-la à nouveau depuis la bibliothèque",
"Coverage outline": "Contour de couverture",
"DRAPED": "DRAPÉ",
"Below 250 m": "Moins de 250 m",
"250–500 m": "250–500 m",
"500–1,000 m": "500–1 000 m",
"1,000–2,000 m": "1 000–2 000 m",
"2,000–3,000 m": "2 000–3 000 m",
"3,000 m and above": "3 000 m et plus",
"Exit cockpit to fly to a layer": "Quittez le cockpit pour rejoindre une couche",
"500–1,000 m": "500–1 000 m",
"1,000–2,000 m": "1 000–2 000 m",
"2,000–3,000 m": "2 000–3 000 m",
"3,000 m and above": "3 000 m et plus",
"Exit cockpit to fly to a layer": "Quittez le cockpit pour voler vers une couche",
"Library": "Bibliothèque",
"Environment": "Environnement",
"Imagery": "Imagerie",
@@ -3649,11 +3647,11 @@
},
"switch on a dataset to load features": "activez un jeu de données pour charger des entités",
"Switched on by this instance · part of the CCTV layer": "Activé par cette instance · fait partie de la couche CCTV",
"Operator opt-in: set {env}=1 on the server": "Activation par l'opérateur : définir {env}=1 sur le serveur",
"Operator opt-in: add {id} to GEV_LIBRARY_ENABLE on the server": "Activation par l'opérateur : ajouter {id} à GEV_LIBRARY_ENABLE sur le serveur",
"Operator opt-in: set {env}=1 on the server": "Activation par l'opérateur : définir {env}=1 sur le serveur",
"Operator opt-in: add {id} to GEV_LIBRARY_ENABLE on the server": "Activation par l'opérateur : ajouter {id} à GEV_LIBRARY_ENABLE sur le serveur",
"Turned off by this instance's operator": "Désactivé par l'opérateur de cette instance",
"Needs a provider key on the server": "Nécessite une clé du fournisseur sur le serveur",
"Needs this app's server · not available in a static build": "Nécessite le serveur de cette application · indisponible dans une version statique",
"Needs this app's server · not available in a static build": "Nécessite le serveur de cette app · indisponible dans une version statique",
"Checking what this instance serves…": "Vérification de ce que propose cette instance…",
"Regional": "Régional",
"Orthophoto": "Orthophoto",
@@ -3663,13 +3661,13 @@
"True orthophoto": "Orthophoto vraie",
"Type": "Type",
"Resolution": "Résolution",
"Flown": "Prise de vue",
"Flown": "Vol",
"Updates": "Mise à jour",
"Ortho quality": "Qualité ortho",
"DATA LIBRARY": "BIBLIOTHÈQUE DE DONNÉES",
"Close data library": "Fermer la bibliothèque de données",
"Data Library at a glance": "La bibliothèque de données en un coup d'œil",
"Live sources beyond the core layers. Each one is fetched from its provider while it is on; nothing is stored in this app. Add one and it appears in Data Layers under Library.": "Sources en direct au-delà des couches principales. Chacune est récupérée auprès de son fournisseur tant qu'elle est active ; rien n'est stocké dans cette application. Ajoutez-en une et elle apparaît dans le panneau Couches, sous Bibliothèque.",
"Live sources beyond the core layers. Each one is fetched from its provider while it is on; nothing is stored in this app. Add one and it appears in Data Layers under Library.": "Sources en direct au-delà des couches principales. Chacune est récupérée auprès de son fournisseur tant qu'elle est active ; rien n'est stocké dans cette app. Ajoutez-en une et elle apparaît dans Couches de données, sous Bibliothèque.",
"Name, provider or country": "Nom, fournisseur ou pays",
"Theme": "Thème",
"All themes": "Tous les thèmes",
@@ -3681,16 +3679,16 @@
"Zoom to {name}": "Zoomer sur {name}",
"WORKING…": "EN COURS…",
"ADD": "AJOUTER",
"Remove {name} from Data Layers": "Retirer {name} du panneau Couches",
"Add {name} to Data Layers": "Ajouter {name} au panneau Couches",
"Remove {name} from Data Layers": "Retirer {name} des Couches de données",
"Add {name} to Data Layers": "Ajouter {name} aux Couches de données",
"CONFIRM · REMOVE ALL {count}": {
"one": "CONFIRMER · RETIRER {count}",
"many": "CONFIRMER · RETIRER LES {count}",
"other": "CONFIRMER · RETIRER LES {count}"
},
"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}": "Affichées : {shown} sur {total}",
"Confirm: remove every added entry from Data Layers": "Confirmer : retirer des Couches de données toutes les entrées ajoutées",
"Remove every added entry from Data Layers": "Retirer des Couches de données toutes les entrées ajoutées",
"Showing {shown} of {total}": "{shown} sur {total} affichés",
"BROWSE DATA LIBRARY": "PARCOURIR LA BIBLIOTHÈQUE DE DONNÉES"
}
+47 -49
View File
@@ -3293,17 +3293,17 @@
"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": "לחץ על \"פתח\" כדי לטעון את התמונה",
"Press OPEN to load the photo": "לחץ על פתח כדי לטעון את התמונה",
"Captured {month}": "צולם ב-{month}",
"this spot": "המקום הזה",
"the view centre": "מרכז התצוגה",
"{source} at {place}": "{source}: {place}",
"Street level at {place}": "צילומי רחוב: {place}",
"{source} at {place}": "{source} ב-{place}",
"Street level at {place}": "רמת רחוב ב-{place}",
"Switching map": "מחליף מפה",
"Opening imagery": "פותח צילומים",
"Street View is ready: press {open} to load it": "Street View מוכן: לחץ על \"{open}\" כדי לטעון אותו",
"{source} cannot open this link's street-level photo without a working key": "{source} לא יכול לפתוח את תמונת הרחוב שבקישור הזה בלי מפתח תקף",
"Street-level photos are hidden in the current view. Leave it first.": "תמונות הרחוב מוסתרות בתצוגה הנוכחית. צא ממנה קודם.",
"Street View is ready: press {open} to load it": "Street View מוכן: לחץ על {open} כדי לטעון אותו",
"{source} cannot open this link's street-level photo without a working key": "{source} לא יכול לפתוח את תמונת רמת הרחוב של הקישור הזה בלי מפתח תקף",
"Street-level photos are hidden in the current view. Leave it first.": "תמונות ברמת הרחוב מוסתרות בתצוגה הנוכחית. צא ממנה קודם.",
"Ship bridge view": "תצוגת גשר האונייה",
"Satellite orbital view": "התצוגה המסלולית של הלוויין",
"Exit bridge view": "יציאה מתצוגת הגשר",
@@ -3383,10 +3383,8 @@
"Next {stop} {time} {relative}": "התחנה הבאה {stop} {time} {relative}",
"Prev {stop} {time} {relative}": "התחנה הקודמת {stop} {time} {relative}",
"(+{minutes} min late)": "(+{minutes} דק׳ איחור)",
"Exit cockpit to fly to a train": "צא מתא הטייס כדי לטוס אל רכבת",
"Exit cockpit to fly to a train": "צא מתא הטייס כדי לטוס לרכבת",
"Photo © {photographer}": "צילום © {photographer}",
"Photo: {photographer} · {licence}": "צילום: {photographer} · {licence}",
"Photo · {licence}": "צילום · {licence}",
"1 h/s": "שעה/שנ׳",
"1 d/s": "יום/שנ׳",
"1 wk/s": "שבוע/שנ׳",
@@ -3422,16 +3420,16 @@
"{provider} limit reached for today": "המכסה היומית של {provider} נוצלה",
"News resumes tomorrow, or with a paid plan": "החדשות יחזרו מחר, או עם תוכנית בתשלום",
"{provider} refused its key": "{provider} דחה את המפתח",
"Check {envVars} in Provider Settings": "בדוק את {envVars} בהגדרות הספקים",
"Check {envVars} in Provider Settings": "בדקו את {envVars} בהגדרות הספקים",
"{provider} key required": "נדרש מפתח של {provider}",
"Developing: {name}": "מתפתח: {name}",
"{count} events today": {
"one": "{count} אירוע היום",
"one": "אירוע {count} היום",
"two": "{count} אירועים היום",
"other": "{count} אירועים היום"
},
"{count} reports today": {
"one": "{count} דיווח היום",
"one": "דיווח {count} היום",
"two": "{count} דיווחים היום",
"other": "{count} דיווחים היום"
},
@@ -3440,12 +3438,12 @@
"via {source}": "דרך {source}",
"Video": "וידאו",
"Photos": "תמונות",
"Claimed by {who}": "לטענת {who}",
"Claimed": "טענה",
"Claimed by {who}": "נטען על ידי {who}",
"Claimed": "נטען",
"Not yet confirmed by others": "טרם אושר על ידי אחרים",
"Reported by {side} sources": "דווח על ידי מקורות: {side}",
"Reported by {sides}": "דווח על ידי מקורות: {sides}",
"{side} sources": "{side}",
"Reported by {side} sources": "דווח על ידי מקורות {side}",
"Reported by {sides}": "דווח על ידי {sides}",
"{side} sources": "מקורות {side}",
"Official": "רשמי",
"Reported by both sides": "דווח על ידי שני הצדדים",
"Independently reported": "דווח באופן עצמאי",
@@ -3481,7 +3479,7 @@
"{minutes} min": "{minutes} דק׳",
"{hours} h": "{hours} שע׳",
"{count} sources": {
"one": "{count} מקור",
"one": "מקור {count}",
"two": "{count} מקורות",
"other": "{count} מקורות"
},
@@ -3489,35 +3487,35 @@
"News feed not configured on this server": "פיד החדשות אינו מוגדר בשרת זה",
"Malformed news hotspots response": "תגובת מוקדי החדשות פגומה",
"Malformed news feed response": "תגובת פיד החדשות פגומה",
"Exit cockpit to fly to a news report": "צא מתא הטייס כדי לטוס אל דיווח חדשותי",
"Exit cockpit to fly to a news report": "צאו מתא הטייס כדי לטוס אל דיווח חדשותי",
"{source} · LAST {hours} H": "{source} · {hours} השעות האחרונות",
"CACHED · REFRESH FAILED": "מהמטמון · הרענון נכשל",
"{source} · CACHED {age}": "{source} · מהמטמון {age}",
"NO NEWS IN THE LAST {hours} H": "אין חדשות ב-{hours} השעות האחרונות",
"{place} (placed by model)": "{place} (מיקום לפי המודל)",
"NO NEWS IN THE LAST {hours} H": "אין חדשות ב־{hours} השעות האחרונות",
"{place} (placed by model)": "{place} (מוקם על ידי המודל)",
"English headline by the model": "כותרת באנגלית מאת המודל",
"Not placed on the map": "ללא מיקום על המפה",
"Not placed on the map": "לא מוקם על המפה",
"Close": "סגירה",
"Conflicts": "עימותים",
"Local": "מקומי",
"Any severity": "כל רמות החומרה",
"Any severity": "כל חומרה",
"{severity} and above": "{severity} ומעלה",
"{severity} only": "{severity} בלבד",
"All categories": "כל הקטגוריות",
"Last {hours} h": "{hours} שע׳ אחורה",
"News from {area}": "חדשות מ-{area}",
"Within {km} km of the view centre": "בטווח {km} ק\"מ ממרכז התצוגה",
"Last {hours} h": "{hours} שע׳ אחרונות",
"News from {area}": "חדשות מ{area}",
"Within {km} km of the view centre": "בטווח {km} ק״מ ממרכז התצוגה",
"Reading the news…": "קורא את החדשות…",
"News, last {hours} hours": "חדשות, {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": "לא דווח דבר בסביבה בחלון של {hours} שע׳",
"Zoom in to see local news": "התקרבו כדי לראות חדשות מקומיות",
"Nothing reported near here in the last {hours} h": "לא דווח דבר בסביבה ב־{hours} השעות האחרונות",
"Latest news": "חדשות אחרונות",
"No news matches these filters in the last {hours} h": "אין חדשות התואמות למסננים אלה בחלון של {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": "צבע: חומרה · סמל: סוג האירוע",
@@ -3531,8 +3529,8 @@
"After 1945": "אחרי 1945",
"Date unknown": "תאריך לא ידוע",
"Position: centre of the protected area": "מיקום: מרכז השטח המוגן",
"Position approximate (within about {km} km)": "מיקום משוער (בטווח של כ-{km} ק\"מ)",
"Position surveyed (within about {m} m)": "מיקום שנמדד (בטווח של כ-{m} מ׳)",
"Position approximate (within about {km} km)": "מיקום משוער (בטווח של כ־{km} ק״מ)",
"Position surveyed (within about {m} m)": "מיקום שנמדד (בטווח של כ־{m} מ׳)",
"Shipwreck": "ספינה טרופה",
"Fatalities: {count}": "הרוגים: {count}",
"Survivors: {count}": "ניצולים: {count}",
@@ -3549,32 +3547,32 @@
"Click the card to open the source ↗": "לחץ על הכרטיס כדי לפתוח את המקור ↗",
"All years": "כל השנים",
"Period": "תקופה",
"From": "מ-",
"From": "מ־",
"First year of the shipwreck period": "השנה הראשונה בתקופת הטביעות",
"To": "עד",
"Last year of the shipwreck period": "השנה האחרונה בתקופת הטביעות",
"Wikidata unavailable; showing EMODnet only": "Wikidata אינו זמין; מוצג EMODnet בלבד",
"EMODnet unavailable; showing Wikidata only": "EMODnet אינו זמין; מוצג Wikidata בלבד",
"Wikidata unavailable; showing EMODnet only": "ויקינתונים אינו זמין; מוצג EMODnet בלבד",
"EMODnet unavailable; showing Wikidata only": "EMODnet אינו זמין; מוצג ויקינתונים בלבד",
"Shipwrecks unavailable": "ספינות טרופות לא זמינות",
"{count} wrecks": {
"one": "{count} ספינה טרופה",
"two": "{count} ספינות טרופות",
"other": "{count} ספינות טרופות"
},
"EMODnet (European waters) · Wikidata (worldwide)": "EMODnet (מימי אירופה) · Wikidata (כל העולם)",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "ספינות טרופות אירופיות בעלות ערך מורשת מגיעות מ-EMODnet Human Activities; ספינות טרופות בולטות מכל העולם מגיעות מ-Wikidata. הצבע מראה מתי הספינה טבעה; הסמן גדל ככל שמספר ההרוגים עולה. לחץ על סמן כדי לפתוח את הכרטיס שלו.",
"EMODnet (European waters) · Wikidata (worldwide)": "EMODnet (מימי אירופה) · ויקינתונים (כל העולם)",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "ספינות טרופות אירופיות בעלות ערך מורשת מגיעות מ־EMODnet Human Activities; ספינות טרופות בולטות מכל העולם מגיעות מוויקינתונים. הצבע מראה מתי הספינה טבעה; הסמן גדל ככל שמספר ההרוגים עולה. לחץ על סמן כדי לפתוח את הכרטיס שלו.",
"The shared flight isn't reporting now — showing where it was last seen": "הטיסה המשותפת לא מדווחת עכשיו — מוצג המיקום האחרון הידוע",
"The shared flight isn't reporting now — showing where it was at {time}": "הטיסה המשותפת לא מדווחת עכשיו — מוצג המיקום נכון ל-{time}",
"The shared flight isn't reporting now — showing where it was at {time}": "הטיסה המשותפת לא מדווחת עכשיו — מוצג המיקום בשעה {time}",
"The shared military flight isn't reporting now — showing where it was last seen": "הטיסה הצבאית המשותפת לא מדווחת עכשיו — מוצג המיקום האחרון הידוע",
"The shared military flight isn't reporting now — showing where it was at {time}": "הטיסה הצבאית המשותפת לא מדווחת עכשיו — מוצג המיקום נכון ל-{time}",
"The shared vessel isn't reporting now — showing where it was last seen": "האונייה המשותפת לא מדווחת עכשיו — מוצג המיקום האחרון הידוע",
"The shared vessel isn't reporting now — showing where it was at {time}": "האונייה המשותפת לא מדווחת עכשיו — מוצג המיקום נכון ל-{time}",
"The shared military flight isn't reporting now — showing where it was at {time}": "הטיסה הצבאית המשותפת לא מדווחת עכשיו — מוצג המיקום בשעה {time}",
"The shared vessel isn't reporting now — showing where it was last seen": "כלי השיט המשותף לא מדווח עכשיו — מוצג המיקום האחרון הידוע",
"The shared vessel isn't reporting now — showing where it was at {time}": "כלי השיט המשותף לא מדווח עכשיו — מוצג המיקום בשעה {time}",
"The shared satellite isn't reporting now — showing where it was last seen": "הלוויין המשותף לא מדווח עכשיו — מוצג המיקום האחרון הידוע",
"The shared satellite isn't reporting now — showing where it was at {time}": "הלוויין המשותף לא מדווח עכשיו — מוצג המיקום נכון ל-{time}",
"The shared satellite isn't reporting now — showing where it was at {time}": "הלוויין המשותף לא מדווח עכשיו — מוצג המיקום בשעה {time}",
"ICAO: {id}": "ICAO: {id}",
"NORAD: {id}": "NORAD: {id}",
"MMSI: {id}": "MMSI: {id}",
"Vessel UUID: {id}": "UUID של האונייה: {id}",
"Vessel UUID: {id}": "UUID של כלי השיט: {id}",
"Latitude must be within ±90° and longitude within ±180°": "קו הרוחב חייב להיות בטווח ±90° וקו האורך בטווח ±180°",
"Street Level is unavailable": "Street Level אינו זמין",
"No street-level imagery source is configured": "לא הוגדר מקור לצילומי רחוב",
@@ -3641,13 +3639,13 @@
"other": "{count} על המפה"
},
"none yet": "אין עדיין",
"Map features": "אובייקטים במפה",
"Map features": "ישויות במפה",
"from {count} datasets": {
"one": "מ-{count} מערך נתונים",
"two": "מ-{count} מערכי נתונים",
"other": "מ-{count} מערכי נתונים"
},
"switch on a dataset to load features": "הפעל מערך נתונים כדי לטעון אובייקטים",
"switch on a dataset to load features": "הפעל מערך נתונים כדי לטעון ישויות",
"Switched on by this instance · part of the CCTV layer": "מופעל במופע הזה · חלק משכבת CCTV",
"Operator opt-in: set {env}=1 on the server": "הפעלה ביוזמת המפעיל: הגדר {env}=1 בשרת",
"Operator opt-in: add {id} to GEV_LIBRARY_ENABLE on the server": "הפעלה ביוזמת המפעיל: הוסף את {id} ל-GEV_LIBRARY_ENABLE בשרת",
@@ -3657,7 +3655,7 @@
"Checking what this instance serves…": "בודק מה המופע הזה מספק…",
"Regional": "אזורי",
"Orthophoto": "אורתופוטו",
"Satellite mosaic": "פסיפס לווייני",
"Satellite mosaic": "פסיפס לוויני",
"Oblique": "צילום אלכסוני",
"Historic imagery": "צילומים היסטוריים",
"True orthophoto": "אורתופוטו אמיתי",
@@ -3684,9 +3682,9 @@
"Remove {name} from Data Layers": "הסרת {name} משכבות נתונים",
"Add {name} to Data Layers": "הוספת {name} לשכבות נתונים",
"CONFIRM · REMOVE ALL {count}": {
"one": "אישור · הסרה ({count})",
"two": "אישור · הסרת הכול ({count})",
"other": "אישור · הסרת הכול ({count})"
"one": "אישור · הסרת {count}",
"two": "אישור · הסרת כל {count}",
"other": "אישור · הסרת כל {count}"
},
"REMOVE ALL": "הסרת הכול",
"Confirm: remove every added entry from Data Layers": "אישור: הסרת כל הפריטים שנוספו משכבות נתונים",
+65 -67
View File
@@ -1,25 +1,25 @@
{
"News feed unavailable": "Feed delle notizie non disponibile",
"Developing: {name}": "In evoluzione: {name}",
"Developing: {name}": "In sviluppo: {name}",
"{count} events today": {
"one": "{count} evento oggi",
"many": "{count} di eventi oggi",
"many": "{count} eventi oggi",
"other": "{count} eventi oggi"
},
"{count} reports today": {
"one": "{count} notizia oggi",
"many": "{count} di notizie oggi",
"many": "{count} notizie oggi",
"other": "{count} notizie oggi"
},
"Disputed: {name}": "Area contesa: {name}",
"Disputed: {name}": "Conteso: {name}",
"Video": "Video",
"Photos": "Foto",
"Claimed by {who}": "Secondo {who}",
"Claimed": "Affermazione",
"Not yet confirmed by others": "Non ancora confermato da altri",
"Reported by {side} sources": "Riportato da fonti: {side}",
"Reported by {sides}": "Riportato da fonti: {sides}",
"{side} sources": "{side}",
"Reported by {side} sources": "Riportato da fonti {side}",
"Reported by {sides}": "Riportato da {sides}",
"{side} sources": "fonti {side}",
"Official": "Ufficiale",
"Reported by both sides": "Riportato da entrambe le parti",
"Independently reported": "Riportato in modo indipendente",
@@ -53,14 +53,14 @@
"{hours} h": "{hours} h",
"{count} sources": {
"one": "{count} fonte",
"many": "{count} di fonti",
"many": "{count} fonti",
"other": "{count} fonti"
},
"News feed not configured on this server": "Feed delle notizie non configurato su questo server",
"Malformed news hotspots response": "Risposta dei focolai di notizie non valida",
"Malformed news feed response": "Risposta del feed delle notizie non valida",
"{place} (placed by model)": "{place} (posizione stimata dal modello)",
"English headline by the model": "Titolo tradotto in inglese dal modello",
"{place} (placed by model)": "{place} (collocato dal modello)",
"English headline by the model": "Titolo inglese del modello",
"Not placed on the map": "Non collocato sulla mappa",
"Close": "Chiudi",
"Any severity": "Qualsiasi gravità",
@@ -68,8 +68,8 @@
"{severity} only": "Solo {severity}",
"All categories": "Tutte le categorie",
"Reading the news…": "Lettura delle notizie…",
"News, last {hours} hours": "Notizie, periodo in ore: {hours}",
"Last {hours} h": "Periodo: {hours} h",
"News, last {hours} hours": "Notizie delle ultime {hours} ore",
"Last {hours} h": "Ultime {hours} h",
"Time window": "Periodo",
"Conflicts": "Conflitti",
"Local": "Locale",
@@ -78,16 +78,16 @@
"Hotspots": "Focolai",
"Severity": "Gravità",
"Category": "Categoria",
"Nothing reported near here in the last {hours} h": "Nessuna segnalazione qui vicino · periodo: {hours} h",
"Nothing reported near here in the last {hours} h": "Nulla segnalato qui vicino nelle ultime {hours} h",
"Latest news": "Ultime notizie",
"No news matches these filters in the last {hours} h": "Nessuna notizia corrisponde a questi filtri · periodo: {hours} h",
"No news matches these filters in the last {hours} h": "Nessuna notizia delle ultime {hours} h corrisponde a questi filtri",
"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",
"Solid rim: reported by both sides or independently. Dashed rim: a claim. Thin rim: one source, or one side’s own press.": "Bordo pieno: riportato da entrambe le parti o in modo indipendente. Bordo tratteggiato: un'affermazione. Bordo sottile: una fonte, o la stampa di una parte.",
"Solid rim: reported by both sides or independently. Dashed rim: a claim. Thin rim: one source, or one side’s own press.": "Bordo pieno: riportato da entrambe le parti o in modo indipendente. Bordo tratteggiato: un’affermazione. Bordo sottile: una fonte, o la stampa di una parte.",
"Larger: more sources · fainter: older. Picture card: a major or fast-growing event with two or more sources.": "Più grande: più fonti · più tenue: più vecchio. Scheda con immagine: un evento grave o in rapida crescita con due o più fonti.",
"Side labels describe the source, not whether a report is true.": "Le etichette delle parti descrivono la fonte, non se la notizia è vera.",
"Exit cockpit to fly to a news report": "Esci dal cockpit per volare a una notizia",
"Side labels describe the source, not whether a report is true.": "Le etichette di parte descrivono la fonte, non se la notizia è vera.",
"Exit cockpit to fly to a news report": "Esci dalla cabina per volare a una notizia",
"News items in the feed": "Notizie nel feed",
"Pick a shape, then click the map.": "Scegli una forma, poi fai clic sulla mappa.",
"Enter to place the pin, Esc to cancel.": "Premi Enter per posizionare il segnaposto, Esc per annullare.",
@@ -1230,8 +1230,8 @@
"Hybrid": "Ibrido",
"Lines": "Linee",
"Dots": "Punti",
"Road lines colored by live flow: green to red": "Strade colorate in base al flusso live: dal verde al rosso",
"Moving dots colored by live flow": "Punti in movimento colorati in base al flusso live",
"Road lines colored by live flow: green to red": "Strade colorate in base al traffico in tempo reale: dal verde al rosso",
"Moving dots colored by live flow": "Punti in movimento colorati in base al traffico in tempo reale",
"Moving dots": "Punti in movimento",
"TomTom key unavailable": "Chiave TomTom non disponibile",
"TomTom daily budget reached": "Budget giornaliero TomTom esaurito",
@@ -1754,9 +1754,9 @@
"other": "{count} telecamere caricate · attiva CCTV per attivarle"
},
"{count} more cameras outside this view · pan or zoom out": {
"one": "{count} altra telecamera fuori da questa vista · sposta la mappa o riduci lo zoom",
"many": "Altre {count} di telecamere fuori da questa vista · sposta la mappa o riduci lo zoom",
"other": "Altre {count} telecamere fuori da questa vista · sposta la mappa o riduci lo zoom"
"one": "Altra {count} telecamera fuori da questa vista · sposta o riduci lo zoom",
"many": "Altre {count} di telecamere fuori da questa vista · sposta o riduci lo zoom",
"other": "Altre {count} telecamere fuori da questa vista · sposta o riduci lo zoom"
},
"Enable CCTV to load camera intersections": "Attiva CCTV per caricare le telecamere agli incroci",
"Enable CCTV to start camera-linked intelligence summaries.": "Attiva CCTV per avviare i riepiloghi intelligence collegati alle telecamere.",
@@ -1796,7 +1796,7 @@
"{scope} · INSTALLATIONS {coverage}": "{scope} · SITI {coverage}",
"MILITARY FEED OFF · PARTIAL PICTURE": "FEED MILITARE DISATTIVATO · QUADRO PARZIALE",
"FLIGHTS FEED OFF · PARTIAL PICTURE": "FEED VOLI DISATTIVATO · QUADRO PARZIALE",
"NEARBY AIRCRAFT INCOMPLETE · NOT AN ALL-CLEAR": "DATI AEREI VICINI INCOMPLETI · NON È UN VIA LIBERA",
"NEARBY AIRCRAFT INCOMPLETE · NOT AN ALL-CLEAR": "AEREI VICINI INCOMPLETI · NON È UN VIA LIBERA",
"CONTEXT STANDBY": "CONTESTO IN ATTESA",
"ENABLE GLOBAL CONTEXT FOR PROXIMITY PINGS": "ATTIVA IL CONTESTO GLOBALE PER I PING DI PROSSIMITÀ",
"CONTACT LOST · LAST KNOWN READOUT · NOT AN ALL-CLEAR": "CONTATTO PERSO · ULTIMA LETTURA NOTA · NON È UN VIA LIBERA",
@@ -1881,7 +1881,7 @@
"TRACK ACQUIRED": "TRACCIA ACQUISITA",
"{label} · COURSE {heading}°": "{label} · ROTTA {heading}°",
"{label} · HEADING {heading}°": "{label} · PRUA {heading}°",
"{label} · NO HEADING": "{label} · PRUA N/D",
"{label} · NO HEADING": "{label} · NESSUNA PRUA",
"{label} · NADIR VIEW": "{label} · VISTA AL NADIR",
"Context could not restore every layer; try again": "Il Contesto non ha potuto ripristinare tutti i livelli; riprova",
"No selected data layers": "Nessun livello dati selezionato",
@@ -2839,8 +2839,8 @@
"Aircraft cockpit view": "Vista cockpit dell'aereo",
"Ship bridge view": "Vista dalla plancia della nave",
"Satellite orbital view": "Vista orbitale dal satellite",
"LOOK STBD {bearing}°": "VISTA A DRITTA {bearing}°",
"LOOK PORT {bearing}°": "VISTA A SINISTRA {bearing}°",
"LOOK STBD {bearing}°": "VISTA DRITTA {bearing}°",
"LOOK PORT {bearing}°": "VISTA SINISTRA {bearing}°",
"LOOK ASTERN": "VISTA A POPPA",
"UP {angle}°": "SU {angle}°",
"DOWN {angle}°": "GIÙ {angle}°",
@@ -2852,7 +2852,7 @@
"BRIDGE WINDOW · FWD": "FINESTRA PLANCIA · AVANTI",
"OPTICAL PLANE · 01": "PIANO OTTICO · 01",
"HEADING HOLD · AIS": "BLOCCO PRUA · AIS",
"NO HEADING · AIS": "PRUA N/D · AIS",
"NO HEADING · AIS": "NESSUNA PRUA · AIS",
"VISOR LOCK · ACTIVE": "BLOCCO VISIERA · ATTIVO",
"GROUND SPEED · KTS": "VEL. AL SUOLO · KTS",
"SPEED OVER GROUND · KTS": "VEL. SUL FONDO · KTS",
@@ -2989,7 +2989,7 @@
"AVAILABLE MISSIONS": "MISSIONI DISPONIBILI",
"SELECT A MISSION TO INSPECT": "SELEZIONA UNA MISSIONE DA ESAMINARE",
"LOADING 30-DAY MISSION INDEX": "CARICAMENTO INDICE MISSIONI A 30 GIORNI",
"Space Missions unavailable: the mission index could not load. Try again later.": "Missioni spaziali non disponibili: impossibile caricare l'indice delle missioni. Riprova più tardi.",
"Space Missions unavailable: the mission index could not load. Try again later.": "Missioni spaziali non disponibili: impossibile caricare l’indice delle missioni. Riprova più tardi.",
"30-DAY MISSION INDEX UNAVAILABLE": "INDICE MISSIONI A 30 GIORNI NON DISPONIBILE",
"TAB PREVIEWS · ENTER / SPACE SELECTS": "TAB: ANTEPRIMA · ENTER / SPACE: SELEZIONA",
"Internet radio and local SDR": "Radio internet e SDR locale",
@@ -3010,7 +3010,7 @@
"Radio volume": "Volume radio",
"STATION SITE": "SITO STAZIONE",
"DIRECTORY: RADIO BROWSER": "ELENCO: RADIO BROWSER",
"Audio connects directly to the broadcaster when you press play, choose a station on the globe or ask voice to play. Your IP is visible to that broadcaster.": "L'audio si collega direttamente all'emittente quando premi play, scegli una stazione sul globo o chiedi la riproduzione con un comando vocale. Il tuo IP è visibile a quell'emittente.",
"Audio connects directly to the broadcaster when you press play, choose a station on the globe or ask voice to play. Your IP is visible to that broadcaster.": "L'audio si collega direttamente all'emittente dopo aver premuto play, scelto una stazione sul globo o chiesto la riproduzione a voce. Il tuo IP è visibile a quell'emittente.",
"Local RTL-SDR receiver": "Ricevitore RTL-SDR locale",
"LOCAL RTL-SDR": "RTL-SDR LOCALE",
"Chrome/Edge · WebUSB · one local tuner shared by FM and the Local ADS-B layer.": "Chrome/Edge · WebUSB · un sintonizzatore locale condiviso tra FM e il livello ADS-B locale.",
@@ -3207,7 +3207,7 @@
"Left panels": "Pannelli a sinistra",
"Right panels": "Pannelli a destra",
"CONFIRMED": "CONFERMATA",
"PAD / RTLS": "RAMPA / RTLS",
"PAD / RTLS": "SITO DI LANCIO / RTLS",
"fire confidence filter|All": "Tutti",
"fire confidence filter|Nominal+": "Nominale+",
"fire confidence filter|High": "Alta",
@@ -3372,7 +3372,7 @@
"Too many chat requests. Try again in {seconds} s.": "Troppe richieste di chat. Riprova tra {seconds} s.",
"The provider refused access for this key. Check the key's project, region or permissions.": "Il provider ha negato l'accesso per questa chiave. Controlla progetto, regione o autorizzazioni della chiave.",
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "L'account del provider ha esaurito il credito. Controlla la fatturazione o scegli un altro provider IA in Impostazioni.",
"Last 24 h of news. Each report links to its publisher; places and categories are read from the text and data automatically and can be wrong. Side labels, where shown, describe the source, not whether a report is true.": "Ultime 24 h di notizie. Ogni notizia rimanda al suo editore; luoghi e categorie sono letti automaticamente dal testo e dai dati e possono essere errati. Le etichette delle parti, dove presenti, descrivono la fonte, non se la notizia è vera.",
"Last 24 h of news. Each report links to its publisher; places and categories are read from the text and data automatically and can be wrong. Side labels, where shown, describe the source, not whether a report is true.": "Ultime 24 h di notizie. Ogni notizia rimanda al suo editore; luoghi e categorie sono letti automaticamente dal testo e dai dati e possono essere errati. Le etichette di parte, dove presenti, descrivono la fonte, non se la notizia è vera.",
"Economy": "Economia",
"News from {area}": "Notizie: {area}",
"Within {km} km of the view centre": "Entro {km} km dal centro della vista",
@@ -3394,7 +3394,7 @@
"Position: centre of the protected area": "Posizione: centro dell'area protetta",
"Position approximate (within about {km} km)": "Posizione approssimativa (entro circa {km} km)",
"Position surveyed (within about {m} m)": "Posizione rilevata (entro circa {m} m)",
"Vessel: {value}": "Nave: {value}",
"Vessel: {value}": "Imbarcazione: {value}",
"What happened: {value}": "Cosa è successo: {value}",
"Depth: {m} m ({quality})": "Profondità: {m} m ({quality})",
"Depth: {m} m": "Profondità: {m} m",
@@ -3414,11 +3414,11 @@
"other": "{count} relitti"
},
"1900–1945": "1900–1945",
"Shipwreck": "Relitto",
"Shipwreck": "Naufragio",
"Fatalities: {count}": "Vittime: {count}",
"Survivors: {count}": "Sopravvissuti: {count}",
"Click the card to open the article ↗": "Fai clic sulla scheda per aprire l'articolo ↗",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "I relitti storici europei provengono da EMODnet Human Activities; i relitti noti di tutto il mondo da Wikidata. Il colore indica quando la nave è affondata; il marcatore cresce con il numero di vittime. Fai clic su un marcatore per la sua scheda.",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "I relitti storici europei provengono da EMODnet Human Activities; i relitti noti di tutto il mondo da Wikidata. Il colore indica quando la nave è affondata; l'indicatore cresce con il numero di vittime. Fai clic su un indicatore per la sua scheda.",
"All years": "Tutti gli anni",
"Period": "Periodo",
"From": "Dal",
@@ -3444,7 +3444,7 @@
"Fungi": "Funghi",
"Other animals": "Altri animali",
"Other or unknown": "Altri o sconosciuti",
"{label} {detail} — click to retry": "{label} {detail}: clic per riprovare",
"{label} {detail} — click to retry": "{label} {detail} — fai clic per riprovare",
"Wildlife sightings source unavailable": "Fonte degli avvistamenti non disponibile",
"Show only community-confirmed (research-grade) sightings": "Mostra solo osservazioni confermate dalla comunità (grado di ricerca)",
"No recent sightings in view": "Nessun avvistamento recente nella vista",
@@ -3463,8 +3463,8 @@
"All rights reserved": "Tutti i diritti riservati",
"Cetacean": "Cetaceo",
"Wildlife Sightings": "Avvistamenti naturalistici",
"Exit cockpit to fly to a sighting": "Esci dal cockpit per volare a un avvistamento",
"Exit cockpit to fly to a sighting area": "Esci dal cockpit per volare a un'area di avvistamento",
"Exit cockpit to fly to a sighting": "Esci dalla cabina per volare a un avvistamento",
"Exit cockpit to fly to a sighting area": "Esci dalla cabina per volare a un’area di avvistamento",
"Wildlife": "Natura",
"Zoom in for local sightings": "Ingrandisci per gli avvistamenti locali",
"Loading photo...": "Caricamento della foto…",
@@ -3473,7 +3473,7 @@
"Photo of {name}": "Foto di {name}",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": "Avvistamenti nella vista: {count}",
"{count} sightings in view": "{count} avvistamenti nella vista",
"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",
@@ -3481,23 +3481,23 @@
"iNaturalist": "iNaturalist",
"iNaturalist contributors · licences per record": "Contributori di iNaturalist · licenze per osservazione",
"iNaturalist contributors · Acartia {count} (Salish Sea) · licences per record": "Contributori di iNaturalist · Acartia {count} (Mare dei Salish) · licenze per osservazione",
"{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": "Età massima degli eventi: {days} g; marcatori senza data mantenuti",
"{days}d": "{days}g",
"All markers": "Tutti gli indicatori",
"Show markers of every age": "Mostra gli indicatori di qualsiasi età",
"Maximum event age: {days}d; undated markers are kept": "Età massima degli eventi: {days} giorni; indicatori senza data mantenuti",
"event age filter|All": "Tutti",
"Event age": "Età degli eventi",
"Event age: {days}d · undated kept": "Età degli eventi: {days} g · senza data mantenuti",
"Event age: {days}d · undated kept": "Età degli eventi: {days} giorni · 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.",
"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}",
"Captured {month}": "Scattata a {month}",
"this spot": "questo punto",
"the view centre": "il centro della vista",
"{source} at {place}": "{source}: {place}",
"Street level at {place}": "Vista stradale: {place}",
"{source} at {place}": "{source} a {place}",
"Street level at {place}": "Vista stradale a {place}",
"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",
@@ -3512,11 +3512,11 @@
"STARLINK": "STARLINK",
"The Starlink constellation (thousands of extra points)": "La costellazione Starlink (migliaia di punti in più)",
"PROBES": "SONDE",
"Space science, technology demonstration and other payloads": "Scienza spaziale, dimostrazioni tecnologiche e altri payload",
"Space science, technology demonstration and other payloads": "Scienza spaziale, dimostrazioni tecnologiche e altri carichi utili",
"DEBRIS": "DETRITI",
"Rocket bodies, fragments and dead satellites": "Stadi di razzi, frammenti e satelliti non operativi",
"Classes may lag: SATCAT is a cached copy": "Le classi potrebbero non essere aggiornate: SATCAT è una copia in cache",
"Classes may lag: SATCAT unavailable": "Le classi potrebbero non essere aggiornate: SATCAT non disponibile",
"Classes may lag: SATCAT is a cached copy": "Le classi potrebbero essere in ritardo: SATCAT è una copia in cache",
"Classes may lag: SATCAT unavailable": "Le classi potrebbero essere in ritardo: SATCAT non disponibile",
"CelesTrak SATCAT gives each object's type and status. Without a current copy the classes rest on the UCS database, names and CelesTrak groups, so a satellite that has died since may still show as working.": "SATCAT di CelesTrak fornisce tipo e stato di ogni oggetto. Senza una copia aggiornata le classi si basano sul database UCS, sui nomi e sui gruppi CelesTrak, quindi un satellite che nel frattempo ha cessato di funzionare può ancora risultare operativo.",
"STARLINK ···": "STARLINK ···",
"STARLINK ✕": "STARLINK ✕",
@@ -3554,32 +3554,30 @@
"Hide {category}": "Nascondi {category}",
"Show {category}": "Mostra {category}",
"Live GPS": "GPS in tempo reale",
"Estimated from station reports": "Posizione stimata dalle segnalazioni delle stazioni",
"Estimated from station reports": "Stimata dalle segnalazioni delle stazioni",
"To {headsign}": "Per {headsign}",
"Next {stop} {time} {relative}": "Prossima fermata {stop} {time} {relative}",
"Prev {stop} {time} {relative}": "Fermata precedente {stop} {time} {relative}",
"(+{minutes} min late)": "(+{minutes} min di ritardo)",
"Exit cockpit to fly to a train": "Esci dal cockpit per volare a un treno",
"Photo © {photographer}": "Foto © {photographer}",
"Photo: {photographer} · {licence}": "Foto: {photographer} · {licence}",
"Photo · {licence}": "Foto · {licence}",
"The shared flight isn't reporting now — showing where it was last seen": "Il volo condiviso non sta trasmettendo ora: viene mostrata l'ultima posizione nota",
"The shared flight isn't reporting now — showing where it was at {time}": "Il volo condiviso non sta trasmettendo ora: viene mostrata la posizione rilevata ({time})",
"The shared flight isn't reporting now — showing where it was at {time}": "Il volo condiviso non sta trasmettendo ora: viene mostrata la posizione alle {time}",
"The shared military flight isn't reporting now — showing where it was last seen": "Il volo militare condiviso non sta trasmettendo ora: viene mostrata l'ultima posizione nota",
"The shared military flight isn't reporting now — showing where it was at {time}": "Il volo militare condiviso non sta trasmettendo ora: viene mostrata la posizione rilevata ({time})",
"The shared military flight isn't reporting now — showing where it was at {time}": "Il volo militare condiviso non sta trasmettendo ora: viene mostrata la posizione alle {time}",
"The shared vessel isn't reporting now — showing where it was last seen": "La nave condivisa non sta trasmettendo ora: viene mostrata l'ultima posizione nota",
"The shared vessel isn't reporting now — showing where it was at {time}": "La nave condivisa non sta trasmettendo ora: viene mostrata la posizione rilevata ({time})",
"The shared vessel isn't reporting now — showing where it was at {time}": "La nave condivisa non sta trasmettendo ora: viene mostrata la posizione alle {time}",
"The shared satellite isn't reporting now — showing where it was last seen": "Il satellite condiviso non sta trasmettendo ora: viene mostrata l'ultima posizione nota",
"The shared satellite isn't reporting now — showing where it was at {time}": "Il satellite condiviso non sta trasmettendo ora: viene mostrata la posizione rilevata ({time})",
"The shared satellite isn't reporting now — showing where it was at {time}": "Il satellite condiviso non sta trasmettendo ora: viene mostrata la posizione alle {time}",
"ICAO: {id}": "ICAO: {id}",
"NORAD: {id}": "NORAD: {id}",
"MMSI: {id}": "MMSI: {id}",
"Vessel UUID: {id}": "UUID della nave: {id}",
"Latitude must be within ±90° and longitude within ±180°": "La latitudine deve essere entro ±90° e la longitudine entro ±180°",
"Street Level is unavailable": "Il livello Vista stradale non è disponibile",
"Street Level is unavailable": "Vista stradale non è disponibile",
"No street-level imagery source is configured": "Nessuna fonte di immagini a livello stradale configurata",
"Street Level could not be enabled": "Impossibile attivare Vista stradale",
"Street Level changed while it looked for imagery": "Il livello Vista stradale è cambiato durante la ricerca delle immagini",
"Street Level changed while it looked for imagery": "Vista stradale è cambiata durante la ricerca delle immagini",
"Aim the view at a street": "Punta la vista su una strada",
"{source} is not a street-level imagery source here": "{source} non è una fonte di immagini a livello stradale qui",
"{source} needs its key": "{source} richiede la sua chiave",
@@ -3591,7 +3589,7 @@
"No {source} imagery near {place}": "Nessuna immagine di {source} vicino a {place}",
"{reason} (filters on: {filters})": "{reason} (filtri attivi: {filters})",
"tiles unavailable": "tile non disponibili",
"zoom in to load this layer": "ingrandisci per caricare questo livello",
"zoom in to load this layer": "avvicinati per caricare questo livello",
"Zoom to layer": "Zoom sul livello",
"Zoom to layer · fly to where this layer has data": "Zoom sul livello · vola dove questo livello ha dati",
"Remove from Data Layers": "Rimuovi da Livelli dati",
@@ -3604,7 +3602,7 @@
"1,000–2,000 m": "1.000–2.000 m",
"2,000–3,000 m": "2.000–3.000 m",
"3,000 m and above": "3.000 m e oltre",
"Exit cockpit to fly to a layer": "Esci dal cockpit per volare a un livello",
"Exit cockpit to fly to a layer": "Esci dalla cabina di pilotaggio per volare a un livello",
"Library": "Libreria",
"Environment": "Ambiente",
"Imagery": "Immagini",
@@ -3652,7 +3650,7 @@
"Operator opt-in: set {env}=1 on the server": "Attivazione dell'operatore: imposta {env}=1 sul server",
"Operator opt-in: add {id} to GEV_LIBRARY_ENABLE on the server": "Attivazione dell'operatore: aggiungi {id} a GEV_LIBRARY_ENABLE sul server",
"Turned off by this instance's operator": "Disattivato dall'operatore di questa istanza",
"Needs a provider key on the server": "Richiede una chiave del provider sul server",
"Needs a provider key on the server": "Richiede una chiave del fornitore sul server",
"Needs this app's server · not available in a static build": "Richiede il server di questa app · non disponibile in una build statica",
"Checking what this instance serves…": "Verifica di ciò che offre questa istanza…",
"Regional": "Regionale",
@@ -3669,8 +3667,8 @@
"DATA LIBRARY": "LIBRERIA DATI",
"Close data library": "Chiudi la libreria dati",
"Data Library at a glance": "La libreria dati in sintesi",
"Live sources beyond the core layers. Each one is fetched from its provider while it is on; nothing is stored in this app. Add one and it appears in Data Layers under Library.": "Fonti live oltre ai livelli principali. Ognuna viene caricata dal suo provider finché è attiva; questa app non memorizza nulla. Aggiungine una e comparirà in Livelli dati, sotto Libreria.",
"Name, provider or country": "Nome, provider o paese",
"Live sources beyond the core layers. Each one is fetched from its provider while it is on; nothing is stored in this app. Add one and it appears in Data Layers under Library.": "Fonti in tempo reale oltre ai livelli principali. Ognuna viene caricata dal suo fornitore finché è attiva; questa app non memorizza nulla. Aggiungine una e comparirà in Livelli dati, sotto Libreria.",
"Name, provider or country": "Nome, fornitore o paese",
"Theme": "Tema",
"All themes": "Tutti i temi",
"Only what this instance serves": "Solo ciò che offre questa istanza",
@@ -3685,12 +3683,12 @@
"Add {name} to Data Layers": "Aggiungi {name} a Livelli dati",
"CONFIRM · REMOVE ALL {count}": {
"one": "CONFERMA · RIMUOVI {count}",
"many": "CONFERMA · RIMUOVI TUTTE LE {count}",
"other": "CONFERMA · RIMUOVI TUTTE LE {count}"
"many": "CONFERMA · RIMUOVI TUTTI I {count}",
"other": "CONFERMA · RIMUOVI TUTTI I {count}"
},
"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}": "Mostrati: {shown} su {total}",
"Showing {shown} of {total}": "{shown} di {total} mostrati",
"BROWSE DATA LIBRARY": "ESPLORA LIBRERIA DATI"
}
+92 -94
View File
@@ -1,21 +1,21 @@
{
"News feed unavailable": "ニュースフィードを利用できません",
"Developing: {name}": "進行中:{name}",
"Developing: {name}": "進行中: {name}",
"{count} events today": {
"other": "本日の出来事 {count}件"
},
"{count} reports today": {
"other": "本日の報道 {count}件"
},
"Disputed: {name}": "係争地:{name}",
"Disputed: {name}": "係争地: {name}",
"Video": "動画",
"Photos": "写真",
"Claimed by {who}": "{who}の主張",
"Claimed": "主張",
"Not yet confirmed by others": "他の情報源による確認なし",
"Reported by {side} sources": "{side}の情報源が報道",
"Reported by {sides}": "報道:{sides}",
"{side} sources": "{side}の情報源",
"Not yet confirmed by others": "他からの確認はまだない",
"Reported by {side} sources": "{side}側の情報源が報道",
"Reported by {sides}": "報道: {sides}",
"{side} sources": "{side}側の情報源",
"Official": "公式",
"Reported by both sides": "双方が報道",
"Independently reported": "独立した報道あり",
@@ -36,13 +36,13 @@
"Air defence": "防空",
"Ground fighting": "地上戦",
"Casualties": "死傷者",
"Humanitarian": "人道問題",
"Humanitarian": "人道",
"Disaster": "災害",
"Protest": "抗議",
"Political": "政治",
"No physical event": "物理的な事象なし",
"Minor incident": "小規模な事案",
"Significant": "中程度",
"Significant": "重大",
"Major": "深刻",
"age|now": "今",
"{minutes} min": "{minutes}分",
@@ -68,7 +68,7 @@
"Conflicts": "紛争",
"Local": "ローカル",
"News sections": "ニュースの区分",
"Zoom in to see local news": "ズームインするとローカルニュースが表示されます",
"Zoom in to see local news": "拡大するとローカルニュースが表示されます",
"Hotspots": "ホットスポット",
"Severity": "深刻度",
"Category": "カテゴリ",
@@ -77,10 +77,10 @@
"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": "色:深刻度 · アイコン:出来事の種類",
"Solid rim: reported by both sides or independently. Dashed rim: a claim. Thin rim: one source, or one side’s own press.": "実線の縁:双方または独立した情報源が報道。破線の縁:主張。細い縁:単一の情報源、または一方の陣営のメディア。",
"Larger: more sources · fainter: older. Picture card: a major or fast-growing event with two or more sources.": "大きい:情報源が多い · 薄い:古い。画像カード:情報源が2件以上ある深刻な、または急拡大中の出来事。",
"Side labels describe the source, not whether a report is true.": "陣営ラベルは情報源を示すもので、報道の真偽を示すものではありません。",
"Colour: severity · icon: type of event": "色: 深刻度 · アイコン: 出来事の種類",
"Solid rim: reported by both sides or independently. Dashed rim: a claim. Thin rim: one source, or one side’s own press.": "実線の縁: 双方または独立した情報源が報道。破線の縁: 主張。細い縁: 単一の情報源、または一方の陣営のメディア。",
"Larger: more sources · fainter: older. Picture card: a major or fast-growing event with two or more sources.": "大きい: 情報源が多い · 薄い: 古い。画像カード: 情報源が2件以上ある深刻な、または急拡大中の出来事。",
"Side labels describe the source, not whether a report is true.": "陣営ラベルは情報源を示すもので、報道の真偽ではありません。",
"Exit cockpit to fly to a news report": "ニュースへ移動するにはコックピットを終了してください",
"News items in the feed": "フィード内のニュース数",
"Pick a shape, then click the map.": "図形を選んでから、地図をクリックしてください。",
@@ -479,7 +479,7 @@
"Remove {title} from this app's saved keys": "{title}をこのアプリの保存済みキーから削除",
"Project keys: {status}": "プロジェクトのキー:{status}",
"your app configuration": "アプリの設定",
"your local .env": "ローカルの.env",
"your local .env": "ローカルの .env",
"Saving…": "保存中…",
"Save failed: {detail}": "保存に失敗しました:{detail}",
"Paste at least one key first.": "まず、キーを1つ以上貼り付けてください。",
@@ -801,7 +801,7 @@
"loading...": "読込中…",
"KEY REQUIRED": "キーが必要",
"LIVE · updated {age}": "ライブ · 更新 {age}",
"24 h to {day} {time} UTC": "{day} {time} UTCまでの24時間",
"24 h to {day} {time} UTC": "{day} {time} UTC までの24時間",
"Source snapshot time unavailable": "ソースのスナップショット時刻を取得できません",
"Live data unavailable": "ライブデータを利用できません",
"Intel Picture": "情勢図",
@@ -831,7 +831,7 @@
"Low-accuracy score 2–10%": "低精度スコア 2–10%",
"Low-accuracy score > 10%": "低精度スコア > 10%",
"GNSS integrity source unavailable": "GNSSインテグリティのソースを利用できません",
"Regional sample · 250 nm radius · cells from visited views over {minutes} min": "地域サンプル · 半径250海里 · 過去{minutes}分間に表示したビューのセル",
"Regional sample · 250 nm radius · cells from visited views over {minutes} min": "地域サンプル · 半径250海里 · {minutes}分以内に表示したビューのセル",
"Retrying automatically at {time} UTC": "{time} UTCに自動で再試行します",
"Retrying automatically after provider cooldown": "プロバイダーのクールダウン後に自動で再試行します",
"Updates automatically after pan or zoom and once a minute. The public feed covers a 250-nautical-mile circle around the view centre; zooming out does not make it a worldwide snapshot. Blank cells are outside sampled coverage or have insufficient observations.": "パンやズームの後、および1分ごとに自動で更新されます。公開フィードは表示中心から半径250海里の円をカバーします。ズームアウトしても全世界のスナップショットにはなりません。空白のセルは、サンプリング範囲外か観測が不十分です。",
@@ -889,18 +889,18 @@
"{value} KM/S": "{value} km/s",
"{direction} {angle}°": "{direction} {angle}°",
"OFF-NADIR {angle}° · {offset}": "オフナディア {angle}° · {offset}",
"SITES STANDBY": "施設待機",
"ENABLE MAPPED INSTALLATIONS FOR SITE PINGS": "マップ済み施設を有効にすると施設ピングを表示",
"Release the camera from {name}": "{name}からカメラを解放",
"SITES STANDBY": "地点待機",
"ENABLE MAPPED INSTALLATIONS FOR SITE PINGS": "マップ済み施設を有効にすると地点ピングを表示",
"Release the camera from {name}": "{name} からカメラを解放",
"RETURN · {name}": "戻る · {name}",
"PREVIOUS SATELLITE": "前の衛星",
"Return to {name}": "{name}に戻る",
"Return to {name}": "{name} に戻る",
"SATELLITE": "衛星",
"Ride {name}": "{name}に乗る",
"Unnamed site": "名称不明の施設",
"Ride {name}": "{name} に乗る",
"Unnamed site": "名称のない地点",
"OFF-NADIR {angle}°": "オフナディア {angle}°",
"FOCUS · {name}": "フォーカス · {name}",
"Keep the camera on {name}": "カメラを{name}に向け続ける",
"Keep the camera on {name}": "カメラを {name} に向け続ける",
"ZOOM {zoom}×": "ズーム {zoom}×",
"SWATH {value} KM": "観測幅 {value} km",
"SWATH ≈ {value} KM": "観測幅 ≈ {value} km",
@@ -909,8 +909,8 @@
"ORBITAL VELOCITY": "軌道速度",
"SUB-SATELLITE POINT": "衛星直下点",
"NORTH UP": "ノースアップ",
"CURRENTLY OVER": "現在の直下",
"{division}, {country}": "{country}・{division}",
"CURRENTLY OVER": "現在の上空",
"{division}, {country}": "{division}、{country}",
"{region} · near {place}": "{region} · {place}付近",
"NORAD {id}": "NORAD {id}",
"EPOCH {time}": "元期 {time}",
@@ -1702,7 +1702,7 @@
"other": "カメラ {count}台を読込済み · CCTVを有効にして使用"
},
"{count} more cameras outside this view · pan or zoom out": {
"other": "表示範囲外にあと{count}台のカメラ · パンまたはズームアウト"
"other": "この表示範囲外にさらに {count}台のカメラ · 移動またはズームアウト"
},
"Enable CCTV to load camera intersections": "CCTVを有効にすると、カメラのある交差点を読み込みます",
"Enable CCTV to start camera-linked intelligence summaries.": "CCTVを有効にすると、カメラ連動のインテリジェンス概要を開始します。",
@@ -1740,8 +1740,8 @@
"REGIONAL NEWS": "地域ニュース",
"{label} · {radius} AIR/SEA WINDOW": "{label} · 航空機/船舶の範囲 {radius}",
"{scope} · INSTALLATIONS {coverage}": "{scope} · 施設 {coverage}",
"MILITARY FEED OFF · PARTIAL PICTURE": "軍用フィード オフ · 部分的な状況",
"FLIGHTS FEED OFF · PARTIAL PICTURE": "フライトフィード オフ · 部分的な状況",
"MILITARY FEED OFF · PARTIAL PICTURE": "軍用フィードオフ · 部分的な状況",
"FLIGHTS FEED OFF · PARTIAL PICTURE": "フライトフィードオフ · 部分的な状況",
"NEARBY AIRCRAFT INCOMPLETE · NOT AN ALL-CLEAR": "付近の航空機情報は不完全 · 安全の保証ではありません",
"CONTEXT STANDBY": "コンテキスト待機",
"ENABLE GLOBAL CONTEXT FOR PROXIMITY PINGS": "グローバルコンテキストを有効にすると近接ピングを表示",
@@ -1876,7 +1876,7 @@
"Earthquakes (24h)": "地震(24時間)",
"Satellites": "衛星",
"Place names": "地名",
"Space Missions (30d)": "宇宙ミッション(30日)",
"Space Missions (30d)": "宇宙ミッション 30日",
"Street Traffic": "道路交通",
"CCTV": "CCTV",
"Radio": "ラジオ",
@@ -2858,7 +2858,7 @@
"Radio volume": "ラジオの音量",
"STATION SITE": "放送局サイト",
"DIRECTORY: RADIO BROWSER": "ディレクトリ:Radio Browser",
"Audio connects directly to the broadcaster when you press play, choose a station on the globe or ask voice to play. Your IP is visible to that broadcaster.": "再生ボタンを押す、地球儀で局を選ぶ、または音声アシスタントに再生を指示すると、ブラウザーが放送局に直接接続します。お使いのIPアドレスは放送局側から見えます。",
"Audio connects directly to the broadcaster when you press play, choose a station on the globe or ask voice to play. Your IP is visible to that broadcaster.": "再生ボタンを押す、地球儀で局を選ぶ、または音声コントロールで再生を指示すると、音声は放送局に直接接続されます。お使いのIPアドレスは放送局側から見えます。",
"Local RTL-SDR receiver": "ローカルRTL-SDR受信機",
"LOCAL RTL-SDR": "ローカルRTL-SDR",
"Chrome/Edge · WebUSB · one local tuner shared by FM and the Local ADS-B layer.": "Chrome/Edge · WebUSB · FMとローカルADS-Bレイヤーで1台のローカルチューナーを共有します。",
@@ -3032,7 +3032,7 @@
"Restricted": "飛行制限区域",
"Prohibited & Restricted Airspace": "飛行禁止・飛行制限区域",
"scene|PLAY": "再生",
"AUTHORING": "シーン作成",
"AUTHORING": "作成",
"Create and edit scenes": "シーンを作成・編集",
"SCENE": "シーン",
"Scene": "シーン",
@@ -3047,7 +3047,7 @@
"Click a shot to select it, double-click to rename.": "ショットをクリックして選択、ダブルクリックで名前を変更します。",
"Play from this scene through the rest of the project": "このシーンからプロジェクトの最後まで再生",
"Stop playback (Esc)": "再生を停止(Esc)",
"Play the shot after the selected one, moving on to the next scene when needed": "選択中のショットの次を再生(必要に応じて次のシーンへ進む)",
"Play the shot after the selected one, moving on to the next scene when needed": "選択中の次のショットを再生(必要に応じて次のシーンへ進む)",
"SAVE & SHARE": "保存と共有",
"Download every scene as a JSON file you can import later": "すべてのシーンを、後でインポートできるJSONファイルとしてダウンロード",
"Load scenes from an exported JSON file": "書き出したJSONファイルからシーンを読み込む",
@@ -3078,7 +3078,7 @@
"Drawn shapes": "描いた図形",
"Audio": "オーディオ",
"UTILITIES": "ユーティリティ",
"Exit cockpit to fly to a drawing": "描いた図形へ移動するにはコックピットを終了してください",
"Exit cockpit to fly to a drawing": "描画の位置へ移動するにはコックピットを終了してください",
"Directions, drawing and your own files": "ルート案内、描画、自分のファイル",
"Voice place search (needs an Overpass server)": "音声による場所検索(Overpassサーバーが必要)",
"OSM Places": "OSMの場所",
@@ -3212,7 +3212,7 @@
"Too many chat requests. Try again in {seconds} s.": "チャットのリクエストが多すぎます。{seconds}秒後にもう一度お試しください。",
"The provider refused access for this key. Check the key's project, region or permissions.": "プロバイダーがこのキーでのアクセスを拒否しました。キーのプロジェクト、リージョン、権限を確認してください。",
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "プロバイダーアカウントのクレジットが不足しています。課金状況を確認するか、設定で別のAIプロバイダーを選んでください。",
"Last 24 h of news. Each report links to its publisher; places and categories are read from the text and data automatically and can be wrong. Side labels, where shown, describe the source, not whether a report is true.": "直近24時間のニュース。各報道は発信元にリンクします。場所とカテゴリは本文とデータから自動で読み取るため誤りがあり得ます。陣営ラベルは、表示される場合、情報源を示すもので、報道の真偽を示すものではありません。",
"Last 24 h of news. Each report links to its publisher; places and categories are read from the text and data automatically and can be wrong. Side labels, where shown, describe the source, not whether a report is true.": "直近24時間のニュース。各報道は発信元にリンクします。場所とカテゴリは本文とデータから自動で読み取るため誤りがあり得ます。陣営ラベルは、表示される場合、情報源を示すもので、報道の真偽ではありません。",
"Economy": "経済",
"News from {area}": "{area}のニュース",
"Within {km} km of the view centre": "表示中心から{km} km以内",
@@ -3228,33 +3228,33 @@
"Precise conflict points in the News layer, beside GDELT’s articles": "ニュースレイヤーの正確な紛争地点(GDELTの記事に加えて)",
"Worldwide": "世界",
"Shipwrecks": "難破船",
"Before 1900": "1900年より前",
"After 1945": "1946年以降",
"Before 1900": "1900年以前",
"After 1945": "1945年以降",
"Date unknown": "日付不明",
"Position: centre of the protected area": "位置:保護区域の中心",
"Position: centre of the protected area": "位置: 保護区域の中心",
"Position approximate (within about {km} km)": "おおよその位置(誤差約{km} km)",
"Position surveyed (within about {m} m)": "測量された位置(誤差約{m} m)",
"Vessel: {value}": "船種:{value}",
"What happened: {value}": "経緯:{value}",
"Depth: {m} m ({quality})": "水深:{m} m({quality})",
"Depth: {m} m": "水深:{m} m",
"Notes: {value}": "備考:{value}",
"Cargo or finds: {value}": "積荷・遺物:{value}",
"Vessel: {value}": "船種: {value}",
"What happened: {value}": "経緯: {value}",
"Depth: {m} m ({quality})": "水深: {m} m({quality})",
"Depth: {m} m": "水深: {m} m",
"Notes: {value}": "備考: {value}",
"Cargo or finds: {value}": "積荷・遺物: {value}",
"Protected wreck": "保護対象の難破船",
"Source: {value}": "出典:{value}",
"Source: {value}": "出典: {value}",
"Click the card to open EMODnet ↗": "カードをクリックしてEMODnetを開く ↗",
"Click the card to open the source ↗": "カードをクリックして出典を開く ↗",
"Wikidata unavailable; showing EMODnet only": "Wikidataを利用できません。EMODnetのみ表示しています",
"EMODnet unavailable; showing Wikidata only": "EMODnetを利用できません。Wikidataのみ表示しています",
"Shipwrecks unavailable": "難破船データを利用できません",
"EMODnet (European waters) · Wikidata (worldwide)": "EMODnet(欧州海域) · Wikidata(世界)",
"EMODnet (European waters) · Wikidata (worldwide)": "EMODnet(欧州海域)· Wikidata(世界)",
"{count} wrecks": {
"other": "難破船 {count}隻"
"other": "難破船 {count}件"
},
"1900–1945": "1900–1945",
"Shipwreck": "難破船",
"Fatalities: {count}": "死者:{count}人",
"Survivors: {count}": "生存者:{count}人",
"Shipwreck": "難破",
"Fatalities: {count}": "死者: {count}",
"Survivors: {count}": "生存者: {count}",
"Click the card to open the article ↗": "カードをクリックして記事を開く ↗",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "欧州の歴史的な難破船はEMODnet Human Activities、世界の著名な難破船はWikidataによります。色は沈没した時期を示し、マーカーは死者数に応じて大きくなります。マーカーをクリックするとカードが開きます。",
"All years": "全期間",
@@ -3277,13 +3277,13 @@
"Amphibians": "両生類",
"Ray-finned fishes": "条鰭類",
"Insects": "昆虫",
"Arachnids": "クモガタ類",
"Arachnids": "クモ類",
"Plants": "植物",
"Fungi": "菌類",
"Other animals": "その他の動物",
"Other or unknown": "その他・不明",
"{label} {detail} — click to retry": "{label} {detail}:クリックで再試行",
"Wildlife sightings source unavailable": "野生生物の目撃情報のソースを利用できません",
"{label} {detail} — click to retry": "{label} {detail} — クリックして再試行",
"Wildlife sightings source unavailable": "野生生物の目撃情報源を利用できません",
"Show only community-confirmed (research-grade) sightings": "コミュニティが確認した研究グレードの目撃情報のみ表示",
"No recent sightings in view": "表示範囲に最近の目撃情報はありません",
"Acartia feed unavailable": "Acartiaのフィードを利用できません",
@@ -3301,10 +3301,10 @@
"All rights reserved": "無断転載禁止",
"Cetacean": "クジラ類",
"Wildlife Sightings": "野生生物の目撃情報",
"Exit cockpit to fly to a sighting": "目撃地点へ移動するにはコックピットを終了してください",
"Exit cockpit to fly to a sighting area": "目撃範囲へ移動するにはコックピットを終了してください",
"Exit cockpit to fly to a sighting": "目撃地点に移動するにはコックピットを終了",
"Exit cockpit to fly to a sighting area": "目撃範囲に移動するにはコックピットを終了",
"Wildlife": "野生生物",
"Zoom in for local sightings": "ズームインして地域の目撃情報を表示",
"Zoom in for local sightings": "拡大して地域の目撃情報を表示",
"Loading photo...": "写真を読み込み中…",
"Photo unavailable": "写真を表示できません",
"Location obscured to a 0.2° cell": "位置は0.2°のセル内に秘匿されています",
@@ -3313,12 +3313,12 @@
"CC BY 4.0": "CC BY 4.0",
"{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°に丸めます。個体数調査ではありません。",
"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}を開く",
"Open {name} on Acartia": "Acartiaで{name}を開く",
"iNaturalist": "iNaturalist",
"iNaturalist contributors · licences per record": "iNaturalistの投稿者 · ライセンスは記録ごと",
"iNaturalist contributors · Acartia {count} (Salish Sea) · licences per record": "iNaturalistの投稿者 · Acartia {count}(セイリッシュ海) · ライセンスは記録ごと",
"iNaturalist contributors · licences per record": "iNaturalist の投稿者 · ライセンスは記録ごと",
"iNaturalist contributors · Acartia {count} (Salish Sea) · licences per record": "iNaturalist の投稿者 · Acartia {count}(セイリッシュ海) · ライセンスは記録ごと",
"{days}d": "{days}日",
"All markers": "すべてのマーカー",
"Show markers of every age": "日付にかかわらずすべてのマーカーを表示",
@@ -3329,8 +3329,8 @@
"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": "[開く]を押して写真を読み込む",
"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": "表示中心",
@@ -3338,8 +3338,8 @@
"Street level at {place}": "{place}のストリートレベル",
"Switching map": "地図を切り替え中",
"Opening imagery": "画像を開いています",
"Street View is ready: press {open} to load it": "ストリートビューの準備ができました:[{open}]を押して読み込んでください",
"{source} cannot open this link's street-level photo without a working key": "{source}は有効なキーがないため、このリンクのストリートレベル写真を開けません",
"Street View is ready: press {open} to load it": "ストリートビューの準備ができました:{open} を押して読み込みます",
"{source} cannot open this link's street-level photo without a working key": "{source} は有効なキーがないため、このリンクのストリートレベル写真を開けません",
"Street-level photos are hidden in the current view. Leave it first.": "現在の表示ではストリートレベルの写真は非表示です。先にこの表示を終了してください。",
"STATIONS": "ステーション",
"Crewed stations and the craft that visit them": "有人宇宙ステーションと、そこを訪れる宇宙機",
@@ -3353,16 +3353,16 @@
"Space science, technology demonstration and other payloads": "宇宙科学、技術実証、その他のペイロード",
"DEBRIS": "デブリ",
"Rocket bodies, fragments and dead satellites": "ロケット機体、破片、運用を終えた衛星",
"Classes may lag: SATCAT is a cached copy": "分類が古い可能性:SATCATはキャッシュのコピーです",
"Classes may lag: SATCAT unavailable": "分類が古い可能性:SATCATを取得できません",
"CelesTrak SATCAT gives each object's type and status. Without a current copy the classes rest on the UCS database, names and CelesTrak groups, so a satellite that has died since may still show as working.": "CelesTrak SATCATは各物体の種別と運用状態を示します。最新のコピーがない間、分類はUCSデータベース、名称、CelesTrakのグループのみに基づくため、その後機能を失った衛星がまだ稼働中と表示されることがあります。",
"Classes may lag: SATCAT is a cached copy": "分類が古い可能性: SATCAT はキャッシュのコピーです",
"Classes may lag: SATCAT unavailable": "分類が古い可能性: SATCAT を取得できません",
"CelesTrak SATCAT gives each object's type and status. Without a current copy the classes rest on the UCS database, names and CelesTrak groups, so a satellite that has died since may still show as working.": "CelesTrak SATCAT は各物体の種別と運用状態を示します。最新のコピーがない間、分類は UCS データベース、名称、CelesTrak のグループのみに基づくため、その後機能を失った衛星がまだ稼働中と表示されることがあります。",
"STARLINK ···": "STARLINK ···",
"STARLINK ✕": "STARLINK ✕",
"SENSOR SWATH {value} KM": "センサー観測幅 {value} km",
"SENSOR SWATH ≈ {value} KM": "センサー観測幅 ≈ {value} km",
"SENSOR": "センサー",
"Zoom to the imaging sensor swath": "撮像センサーの観測幅までズーム",
"<1 H OLD": "1時間以内",
"<1 H OLD": "1時間未満前",
"{value} H OLD": "{value}時間前",
"{value} D OLD": "{value}日前",
"FWD": "前方",
@@ -3371,8 +3371,8 @@
"LEFT": "左",
"TRACK UP": "トラックアップ",
"COURSE HOLD · AIS": "対地針路保持 · AIS",
"Zoom in (+)": "ズームイン(+)",
"Zoom out (−)": "ズームアウト(−)",
"Zoom in (+)": "ズームイン (+)",
"Zoom out (−)": "ズームアウト (−)",
"ORBITAL VIEW · OFF-NADIR": "軌道ビュー · オフナディア",
"World time": "世界時刻",
"Back to live": "ライブに戻る",
@@ -3399,16 +3399,14 @@
"(+{minutes} min late)": "({minutes}分遅れ)",
"Exit cockpit to fly to a train": "列車へ移動するにはコックピットを終了してください",
"Photo © {photographer}": "写真 © {photographer}",
"Photo: {photographer} · {licence}": "写真:{photographer} · {licence}",
"Photo · {licence}": "写真 · {licence}",
"The shared flight isn't reporting now — showing where it was last seen": "共有されたフライトの位置情報は現在届いていません。最後に確認された位置を表示しています",
"The shared flight isn't reporting now — showing where it was at {time}": "共有されたフライトの位置情報は現在届いていません。{time}時点の位置を表示しています",
"The shared military flight isn't reporting now — showing where it was last seen": "共有された軍用機の位置情報は現在届いていません。最後に確認された位置を表示しています",
"The shared military flight isn't reporting now — showing where it was at {time}": "共有された軍用機の位置情報は現在届いていません。{time}時点の位置を表示しています",
"The shared vessel isn't reporting now — showing where it was last seen": "共有された船舶の位置情報は現在届いていません。最後に確認された位置を表示しています",
"The shared vessel isn't reporting now — showing where it was at {time}": "共有された船舶の位置情報は現在届いていません。{time}時点の位置を表示しています",
"The shared satellite isn't reporting now — showing where it was last seen": "共有された衛星の位置情報は現在届いていません。最後に確認された位置を表示しています",
"The shared satellite isn't reporting now — showing where it was at {time}": "共有された衛星の位置情報は現在届いていません。{time}時点の位置を表示しています",
"The shared flight isn't reporting now — showing where it was last seen": "共有されたフライトは現在報告していません。最後に確認された位置を表示しています",
"The shared flight isn't reporting now — showing where it was at {time}": "共有されたフライトは現在報告していません。{time}時点の位置を表示しています",
"The shared military flight isn't reporting now — showing where it was last seen": "共有された軍用機は現在報告していません。最後に確認された位置を表示しています",
"The shared military flight isn't reporting now — showing where it was at {time}": "共有された軍用機は現在報告していません。{time}時点の位置を表示しています",
"The shared vessel isn't reporting now — showing where it was last seen": "共有された船舶は現在報告していません。最後に確認された位置を表示しています",
"The shared vessel isn't reporting now — showing where it was at {time}": "共有された船舶は現在報告していません。{time}時点の位置を表示しています",
"The shared satellite isn't reporting now — showing where it was last seen": "共有された衛星は現在報告していません。最後に確認された位置を表示しています",
"The shared satellite isn't reporting now — showing where it was at {time}": "共有された衛星は現在報告していません。{time}時点の位置を表示しています",
"ICAO: {id}": "ICAO: {id}",
"NORAD: {id}": "NORAD: {id}",
"MMSI: {id}": "MMSI: {id}",
@@ -3427,7 +3425,7 @@
"{source} imagery could not be opened": "{source}の画像を開けませんでした",
"{place} has no position to look for imagery at": "{place}には画像を探す位置がありません",
"No {source} imagery near {place}": "{place}の近くに{source}の画像はありません",
"{reason} (filters on: {filters})": "{reason}(有効なフィルター:{filters})",
"{reason} (filters on: {filters})": "{reason}(有効なフィルター: {filters})",
"tiles unavailable": "タイルを利用できません",
"zoom in to load this layer": "ズームインしてこのレイヤーを読み込む",
"Zoom to layer": "レイヤーにズーム",
@@ -3436,12 +3434,12 @@
"Remove from Data Layers · add it again from the library": "データレイヤーから削除 · ライブラリから再度追加できます",
"Coverage outline": "カバー範囲の輪郭",
"DRAPED": "地形に投影",
"Below 250 m": "250 m未満",
"Below 250 m": "250 m 未満",
"250–500 m": "250–500 m",
"500–1,000 m": "500–1,000 m",
"1,000–2,000 m": "1,000–2,000 m",
"2,000–3,000 m": "2,000–3,000 m",
"3,000 m and above": "3,000 m以上",
"3,000 m and above": "3,000 m 以上",
"Exit cockpit to fly to a layer": "レイヤーへ移動するにはコックピットを終了してください",
"Library": "ライブラリ",
"Environment": "環境",
@@ -3457,28 +3455,28 @@
"Datasets": "データセット",
"{datasets} · {tiles} · {packs}": "{datasets} · {tiles} · {packs}",
"{count} datasets": {
"other": "{count}件のデータセット"
"other": "{count} 件のデータセット"
},
"{count} tile layers": {
"other": "{count}件のタイルレイヤー"
"other": "{count} 件のタイルレイヤー"
},
"{count} packs": {
"other": "{count}件のパック"
"other": "{count} 件のパック"
},
"Themes": "テーマ",
"Added": "追加済み",
"{count} on the map": {
"other": "地図上に{count}件"
"other": "地図上に {count} 件"
},
"none yet": "まだありません",
"Map features": "地図上の地物",
"from {count} datasets": {
"other": "{count}件のデータセットから"
"other": "{count} 件のデータセットから"
},
"switch on a dataset to load features": "データセットをオンにすると地物を読み込みます",
"Switched on by this instance · part of the CCTV layer": "このインスタンスで有効 · CCTVレイヤーの一部",
"Operator opt-in: set {env}=1 on the server": "運用者による有効化:サーバーで{env}=1を設定",
"Operator opt-in: add {id} to GEV_LIBRARY_ENABLE on the server": "運用者による有効化:サーバーのGEV_LIBRARY_ENABLEに{id}を追加",
"Switched on by this instance · part of the CCTV layer": "このインスタンスで有効 · CCTV レイヤーの一部",
"Operator opt-in: set {env}=1 on the server": "運用者による有効化:サーバーで {env}=1 を設定",
"Operator opt-in: add {id} to GEV_LIBRARY_ENABLE on the server": "運用者による有効化:サーバーの GEV_LIBRARY_ENABLE に {id} を追加",
"Turned off by this instance's operator": "このインスタンスの運用者により無効化されています",
"Needs a provider key on the server": "サーバーにプロバイダーのキーが必要です",
"Needs this app's server · not available in a static build": "このアプリのサーバーが必要 · 静的ビルドでは利用できません",
@@ -3506,17 +3504,17 @@
"KEY": "キー必須",
"KEYLESS": "キー不要",
"ZOOM": "ズーム",
"Zoom to {name}": "{name}にズーム",
"Zoom to {name}": "{name} にズーム",
"WORKING…": "処理中…",
"ADD": "追加",
"Remove {name} from Data Layers": "{name}をデータレイヤーから削除",
"Add {name} to Data Layers": "{name}をデータレイヤーに追加",
"Remove {name} from Data Layers": "{name} をデータレイヤーから削除",
"Add {name} to Data Layers": "{name} をデータレイヤーに追加",
"CONFIRM · REMOVE ALL {count}": {
"other": "確認 · {count}件をすべて削除"
"other": "確認 · {count} 件をすべて削除"
},
"REMOVE ALL": "すべて削除",
"Confirm: remove every added entry from Data Layers": "確認:追加したすべての項目をデータレイヤーから削除",
"Remove every added entry from Data Layers": "追加したすべての項目をデータレイヤーから削除",
"Showing {shown} of {total}": "{total}件中{shown}件を表示",
"Showing {shown} of {total}": "{total} 件中 {shown} 件を表示",
"BROWSE DATA LIBRARY": "データライブラリを見る"
}
+63 -65
View File
@@ -3,12 +3,12 @@
"Developing: {name}": "Em desenvolvimento: {name}",
"{count} events today": {
"one": "{count} evento hoje",
"many": "{count} de eventos hoje",
"many": "{count} eventos hoje",
"other": "{count} eventos hoje"
},
"{count} reports today": {
"one": "{count} notícia hoje",
"many": "{count} de notícias hoje",
"many": "{count} notícias hoje",
"other": "{count} notícias hoje"
},
"Disputed: {name}": "Em disputa: {name}",
@@ -17,9 +17,9 @@
"Claimed by {who}": "Segundo {who}",
"Claimed": "Alegação",
"Not yet confirmed by others": "Ainda não confirmado por outros",
"Reported by {side} sources": "Noticiado por fontes: {side}",
"Reported by {side} sources": "Noticiado por fontes de {side}",
"Reported by {sides}": "Noticiado por {sides}",
"{side} sources": "fontes: {side}",
"{side} sources": "fontes de {side}",
"Official": "Oficial",
"Reported by both sides": "Noticiado por ambos os lados",
"Independently reported": "Noticiado de forma independente",
@@ -27,7 +27,7 @@
"International": "Internacional",
"Independent": "Independente",
"Unattributed": "Sem atribuição",
"Outside party": "Terceiros",
"Outside party": "Parte externa",
"Ukraine–Russia": "Ucrânia–Rússia",
"Israel–Gaza–Lebanon": "Israel–Gaza–Líbano",
"Iran–Hormuz": "Irã–Ormuz",
@@ -43,7 +43,7 @@
"Humanitarian": "Humanitário",
"Disaster": "Desastre",
"Protest": "Protesto",
"Political": "Política",
"Political": "Político",
"No physical event": "Sem evento físico",
"Minor incident": "Incidente menor",
"Significant": "Significativo",
@@ -53,7 +53,7 @@
"{hours} h": "{hours} h",
"{count} sources": {
"one": "{count} fonte",
"many": "{count} de fontes",
"many": "{count} fontes",
"other": "{count} fontes"
},
"News feed not configured on this server": "Feed de notícias não configurado neste servidor",
@@ -64,23 +64,23 @@
"Not placed on the map": "Não localizado no mapa",
"Close": "Fechar",
"Any severity": "Qualquer gravidade",
"{severity} and above": "{severity} ou superior",
"{severity} and above": "{severity} ou mais",
"{severity} only": "Apenas {severity}",
"All categories": "Todas as categorias",
"Reading the news…": "Lendo as notícias…",
"News, last {hours} hours": "Notícias de até {hours} h atrás",
"Last {hours} h": "Até {hours} h atrás",
"News, last {hours} hours": "Notícias das últimas {hours} horas",
"Last {hours} h": "Últimas {hours} h",
"Time window": "Período",
"Conflicts": "Conflitos",
"Local": "Local",
"News sections": "Seções de notícias",
"Zoom in to see local news": "Aproxime o zoom para ver notícias locais",
"News sections": "Secções de notícias",
"Zoom in to see local news": "Aproxime para ver notícias locais",
"Hotspots": "Focos",
"Severity": "Gravidade",
"Category": "Categoria",
"Nothing reported near here in the last {hours} h": "Nada noticiado aqui perto desde {hours} h atrás",
"Nothing reported near here in the last {hours} h": "Nada noticiado aqui perto nas últimas {hours} h",
"Latest news": "Últimas notícias",
"No news matches these filters in the last {hours} h": "Nenhuma notícia de até {hours} h atrás corresponde a estes filtros",
"No news matches these filters in the last {hours} h": "Nenhuma notícia das últimas {hours} h 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",
@@ -857,7 +857,7 @@
"Low-accuracy score 2–10%": "Índice de baixa precisão 2–10%",
"Low-accuracy score > 10%": "Índice de baixa precisão > 10%",
"GNSS integrity source unavailable": "Fonte de integridade GNSS indisponível",
"Regional sample · 250 nm radius · cells from visited views over {minutes} min": "Amostra regional · raio de 250 NM · células das áreas visualizadas nos últimos {minutes} min",
"Regional sample · 250 nm radius · cells from visited views over {minutes} min": "Amostra regional · raio de 250 NM · células das vistas visitadas em {minutes} min",
"Retrying automatically at {time} UTC": "Nova tentativa automática às {time} UTC",
"Retrying automatically after provider cooldown": "Nova tentativa automática após a pausa do provedor",
"Updates automatically after pan or zoom and once a minute. The public feed covers a 250-nautical-mile circle around the view centre; zooming out does not make it a worldwide snapshot. Blank cells are outside sampled coverage or have insufficient observations.": "Atualiza automaticamente após mover ou dar zoom e uma vez por minuto. O feed público cobre um círculo de 250 milhas náuticas ao redor do centro da vista; afastar o zoom não o transforma em um snapshot mundial. Células vazias estão fora da cobertura amostrada ou não têm observações suficientes.",
@@ -926,14 +926,14 @@
"PREVIOUS SATELLITE": "SATÉLITE ANTERIOR",
"Return to {name}": "Voltar para {name}",
"SATELLITE": "SATÉLITE",
"Ride {name}": "Ir a bordo de {name}",
"Ride {name}": "Acompanhar {name}",
"Unnamed site": "Local sem nome",
"OFF-NADIR {angle}°": "FORA DO NADIR {angle}°",
"FOCUS · {name}": "FOCO · {name}",
"Keep the camera on {name}": "Manter a câmera em {name}",
"ZOOM {zoom}×": "ZOOM {zoom}×",
"SWATH {value} KM": "FAIXA {value} KM",
"SWATH ≈ {value} KM": "FAIXA ≈ {value} KM",
"SWATH {value} KM": "FAIXA IMAGEADA {value} KM",
"SWATH ≈ {value} KM": "FAIXA IMAGEADA ≈ {value} KM",
"Orbital view": "Vista orbital",
"ORBITAL VIEW · NADIR": "VISTA ORBITAL · NADIR",
"ORBITAL VELOCITY": "VELOCIDADE ORBITAL",
@@ -1234,7 +1234,7 @@
"TomTom key unavailable": "Chave do TomTom indisponível",
"TomTom daily budget reached": "Limite diário do TomTom atingido",
"Monthly traffic allowance used": "Cota mensal de trânsito esgotada",
"Traffic rate limit reached": "Trânsito: limite de requisições atingido",
"Traffic rate limit reached": "Limite de solicitações de trânsito atingido",
"Traffic service unavailable": "Serviço de trânsito indisponível",
"TomTom upstream unreachable": "Servidor do TomTom inacessível",
"TomTom flow unavailable": "Fluxo do TomTom indisponível",
@@ -1754,9 +1754,9 @@
"other": "{count} câmeras carregadas · ative o CCTV para usar"
},
"{count} more cameras outside this view · pan or zoom out": {
"one": "Mais {count} câmera fora desta vista · mova o mapa ou afaste o zoom",
"many": "Mais {count} de câmeras fora desta vista · mova o mapa ou afaste o zoom",
"other": "Mais {count} câmeras fora desta vista · mova o mapa ou afaste o zoom"
"one": "Mais {count} câmera fora desta vista · mova ou afaste o zoom",
"many": "Mais {count} de câmeras fora desta vista · mova ou afaste o zoom",
"other": "Mais {count} câmeras fora desta vista · mova ou afaste o zoom"
},
"Enable CCTV to load camera intersections": "Ative o CCTV para carregar os cruzamentos com câmeras",
"Enable CCTV to start camera-linked intelligence summaries.": "Ative o CCTV para iniciar resumos de inteligência vinculados às câmeras.",
@@ -1796,7 +1796,7 @@
"{scope} · INSTALLATIONS {coverage}": "{scope} · INSTALAÇÕES: {coverage}",
"MILITARY FEED OFF · PARTIAL PICTURE": "FEED MILITAR DESLIGADO · QUADRO PARCIAL",
"FLIGHTS FEED OFF · PARTIAL PICTURE": "FEED DE VOOS DESLIGADO · QUADRO PARCIAL",
"NEARBY AIRCRAFT INCOMPLETE · NOT AN ALL-CLEAR": "LISTA DE AERONAVES PRÓXIMAS INCOMPLETA · NÃO GARANTE SEGURANÇA",
"NEARBY AIRCRAFT INCOMPLETE · NOT AN ALL-CLEAR": "AERONAVES PRÓXIMAS INCOMPLETAS · NÃO GARANTE SEGURANÇA",
"CONTEXT STANDBY": "CONTEXTO EM ESPERA",
"ENABLE GLOBAL CONTEXT FOR PROXIMITY PINGS": "ATIVE O CONTEXTO GLOBAL PARA PINGS DE PROXIMIDADE",
"CONTACT LOST · LAST KNOWN READOUT · NOT AN ALL-CLEAR": "CONTATO PERDIDO · ÚLTIMA LEITURA CONHECIDA · NÃO GARANTE SEGURANÇA",
@@ -2848,7 +2848,7 @@
"DOWN {angle}°": "ABAIXO {angle}°",
"LEVEL": "NIVELADO",
"{direction} · {elevation}": "{direction} · {elevation}",
"LOOK {elevation}": "VISTA · {elevation}",
"LOOK {elevation}": "VISTA {elevation}",
"Estimated destination direction": "Direção estimada do destino",
"DEST ---°": "DEST ---°",
"BRIDGE WINDOW · FWD": "JANELA DO PASSADIÇO · VANTE",
@@ -3012,7 +3012,7 @@
"Radio volume": "Volume do rádio",
"STATION SITE": "SITE DA ESTAÇÃO",
"DIRECTORY: RADIO BROWSER": "DIRETÓRIO: RADIO BROWSER",
"Audio connects directly to the broadcaster when you press play, choose a station on the globe or ask voice to play. Your IP is visible to that broadcaster.": "O áudio se conecta diretamente à emissora quando você pressiona OUVIR, escolhe uma estação no globo ou pede por voz para tocar. Seu IP fica visível para essa emissora.",
"Audio connects directly to the broadcaster when you press play, choose a station on the globe or ask voice to play. Your IP is visible to that broadcaster.": "O áudio conecta direto à emissora quando você aperta \"Ouvir\", escolhe uma estação no globo ou pede por voz. Seu IP fica visível para essa emissora.",
"Local RTL-SDR receiver": "Receptor RTL-SDR local",
"LOCAL RTL-SDR": "RTL-SDR LOCAL",
"Chrome/Edge · WebUSB · one local tuner shared by FM and the Local ADS-B layer.": "Chrome/Edge · WebUSB · um sintonizador local compartilhado entre o FM e a camada ADS-B local.",
@@ -3372,13 +3372,13 @@
"Too many chat requests. Try again in {seconds} s.": "Chat: limite de requisições atingido. Tente novamente em {seconds} s.",
"The provider refused access for this key. Check the key's project, region or permissions.": "O provedor negou o acesso para esta chave. Verifique o projeto, a região ou as permissões da chave.",
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "A conta do provedor está sem crédito. Verifique o faturamento ou escolha outro provedor de IA em \"Configurações\".",
"Last 24 h of news. Each report links to its publisher; places and categories are read from the text and data automatically and can be wrong. Side labels, where shown, describe the source, not whether a report is true.": "Últimas 24 h de notícias. Cada notícia tem um link para o veículo que a publicou; locais e categorias são lidos do texto e dos dados automaticamente e podem estar errados. Os rótulos de lado, quando exibidos, descrevem a fonte, não se a notícia é verdadeira.",
"Last 24 h of news. Each report links to its publisher; places and categories are read from the text and data automatically and can be wrong. Side labels, where shown, describe the source, not whether a report is true.": "Últimas 24 h de notícias. Cada notícia leva ao seu editor; locais e categorias são lidos do texto e dos dados automaticamente e podem estar errados. Os rótulos de lado, quando exibidos, descrevem a fonte, não se a notícia é verdadeira.",
"Economy": "Economia",
"News from {area}": "Notícias: {area}",
"Within {km} km of the view centre": "Até {km} km do centro da vista",
"{region}, {country}": "{region}, {country}",
"via {source}": "via {source}",
"Local in the News layer: the day’s news from the outlets of the country in view": "\"Local\" na camada de notícias: as notícias do dia dos veículos de imprensa do país na vista",
"Local in the News layer: the day’s news from the outlets of the country in view": "Local na camada de notícias: as notícias do dia dos veículos do país em vista",
"{provider} refused its key": "{provider} recusou a chave",
"Check {envVars} in Provider Settings": "Verifique {envVars} em \"Configurações de provedores\"",
"{provider} key required": "Chave do {provider} necessária",
@@ -3402,8 +3402,8 @@
"Cargo or finds: {value}": "Carga ou achados: {value}",
"Protected wreck": "Naufrágio protegido",
"Source: {value}": "Fonte: {value}",
"Click the card to open EMODnet ↗": "Clique no cartão para abrir o EMODnet ↗",
"Click the card to open the source ↗": "Clique no cartão para abrir a fonte ↗",
"Click the card to open EMODnet ↗": "Clique na ficha para abrir o EMODnet ↗",
"Click the card to open the source ↗": "Clique na ficha para abrir a fonte ↗",
"Wikidata unavailable; showing EMODnet only": "Wikidata indisponível; mostrando só o EMODnet",
"EMODnet unavailable; showing Wikidata only": "EMODnet indisponível; mostrando só o Wikidata",
"Shipwrecks unavailable": "Naufrágios indisponíveis",
@@ -3417,8 +3417,8 @@
"Shipwreck": "Naufrágio",
"Fatalities: {count}": "Mortos: {count}",
"Survivors: {count}": "Sobreviventes: {count}",
"Click the card to open the article ↗": "Clique no cartão para abrir o artigo ↗",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Os naufrágios históricos europeus vêm do EMODnet Human Activities; os naufrágios notáveis do mundo vêm do Wikidata. A cor mostra quando o navio afundou; o marcador cresce com o número de mortos. Clique em um marcador para ver o cartão.",
"Click the card to open the article ↗": "Clique na ficha para abrir o artigo ↗",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Os naufrágios históricos europeus vêm do EMODnet Human Activities; os naufrágios notáveis do mundo vêm do Wikidata. A cor mostra quando o navio afundou; o marcador cresce com o número de mortos. Clique num marcador para ver a ficha.",
"All years": "Todos os anos",
"Period": "Período",
"From": "Início",
@@ -3446,7 +3446,7 @@
"Other or unknown": "Outros ou desconhecidos",
"{label} {detail} — click to retry": "{label} {detail} — clique para tentar novamente",
"Wildlife sightings source unavailable": "Fonte de avistamentos indisponível",
"Show only community-confirmed (research-grade) sightings": "Mostrar apenas avistamentos confirmados pela comunidade (grau de pesquisa)",
"Show only community-confirmed (research-grade) sightings": "Mostrar apenas observações confirmadas pela comunidade (grau de pesquisa)",
"No recent sightings in view": "Nenhum avistamento recente na vista",
"Acartia feed unavailable": "Feed Acartia indisponível",
"Show all taxa": "Mostrar todos os grupos taxonômicos",
@@ -3466,28 +3466,28 @@
"Exit cockpit to fly to a sighting": "Saia da cabine para voar até um avistamento",
"Exit cockpit to fly to a sighting area": "Saia da cabine para voar até uma área de avistamento",
"Wildlife": "Vida selvagem",
"Zoom in for local sightings": "Aproxime o zoom para ver avistamentos locais",
"Zoom in for local sightings": "Amplie para ver avistamentos locais",
"Loading photo...": "Carregando foto…",
"Photo unavailable": "Foto indisponível",
"Location obscured to a 0.2° cell": "Localização ocultada em uma célula de 0,2°",
"Photo of {name}": "Foto de {name}",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": "Avistamentos na vista: {count}",
"{count} sightings in view": "{count} avistamentos na vista",
"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.",
"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 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 da Acartia no mar de Salish são arredondados para 0,01°. Não é um censo.",
"Open {name} on iNaturalist": "Abrir {name} no iNaturalist",
"Open {name} on Acartia": "Abrir {name} no Acartia",
"iNaturalist": "iNaturalist",
"iNaturalist contributors · licences per record": "Colaboradores do iNaturalist · licenças por registro",
"iNaturalist contributors · Acartia {count} (Salish Sea) · licences per record": "Colaboradores do iNaturalist · Acartia {count} (mar de Salish) · licenças por registro",
"{days}d": "{days} d",
"{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": "Idade máxima do evento: {days} d; marcadores sem data são mantidos",
"Maximum event age: {days}d; undated markers are kept": "Idade máxima do evento: {days} dias; marcadores sem data mantidos",
"event age filter|All": "Todos",
"Event age": "Idade do evento",
"Event age: {days}d · undated kept": "Idade do evento: {days} d · sem data mantidos",
"Event age: {days}d · undated kept": "Idade do evento: {days} dias · 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.",
@@ -3496,16 +3496,16 @@
"Captured {month}": "Capturada em {month}",
"this spot": "este ponto",
"the view centre": "o centro da vista",
"{source} at {place}": "{source}: {place}",
"Street level at {place}": "Nível da rua: {place}",
"{source} at {place}": "{source} em {place}",
"Street level at {place}": "Nível da rua em {place}",
"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",
"{source} cannot open this link's street-level photo without a working key": "{source} não pode abrir a foto no nível da rua deste link sem uma chave válida",
"Street-level photos are hidden in the current view. Leave it first.": "As fotos no nível da rua estão ocultas na vista atual. Saia dela primeiro.",
"{source} cannot open this link's street-level photo without a working key": "{source} não pode abrir a foto ao nível da rua deste link sem uma chave válida",
"Street-level photos are hidden in the current view. Leave it first.": "As fotos ao nível da rua estão ocultas na vista atual. Saia dela primeiro.",
"STATIONS": "ESTAÇÕES",
"Crewed stations and the craft that visit them": "Estações tripuladas e as naves que as visitam",
"IMAGING": "IMAGEAMENTO",
"IMAGING": "IMAGENS",
"Earth observation: optical, radar, weather and science": "Observação da Terra: óptica, radar, meteorologia e ciência",
"TELECOM": "TELECOM",
"Communications and navigation satellites, Starlink excepted": "Satélites de comunicações e navegação, exceto Starlink",
@@ -3517,7 +3517,7 @@
"Rocket bodies, fragments and dead satellites": "Corpos de foguetes, fragmentos e satélites inativos",
"Classes may lag: SATCAT is a cached copy": "As classes podem estar desatualizadas: SATCAT é uma cópia em cache",
"Classes may lag: SATCAT unavailable": "As classes podem estar desatualizadas: SATCAT indisponível",
"CelesTrak SATCAT gives each object's type and status. Without a current copy the classes rest on the UCS database, names and CelesTrak groups, so a satellite that has died since may still show as working.": "O SATCAT do CelesTrak dá o tipo e o estado de cada objeto. Sem uma cópia atual, as classes se baseiam no banco de dados UCS, nos nomes e nos grupos do CelesTrak, então um satélite que deixou de funcionar pode ainda aparecer como operacional.",
"CelesTrak SATCAT gives each object's type and status. Without a current copy the classes rest on the UCS database, names and CelesTrak groups, so a satellite that has died since may still show as working.": "O SATCAT da CelesTrak dá o tipo e o estado de cada objeto. Sem uma cópia atual, as classes se baseiam no banco de dados UCS, nos nomes e nos grupos da CelesTrak, então um satélite que deixou de funcionar pode ainda aparecer como operacional.",
"STARLINK ···": "STARLINK ···",
"STARLINK ✕": "STARLINK ✕",
"SENSOR SWATH {value} KM": "FAIXA DO SENSOR {value} KM",
@@ -3543,9 +3543,9 @@
"1 d/s": "1 d/s",
"1 wk/s": "1 sem/s",
"Play speed": "Velocidade de reprodução",
"Scroll or pinch to zoom, drag to move along · double-click to see it all": "Role ou use o gesto de pinça para dar zoom, arraste para percorrer · clique duplo para ver tudo",
"Scroll or pinch to zoom, drag to move along · double-click to see it all": "Role ou faça pinça para ampliar, arraste para se mover · clique duplo para ver tudo",
"1 min/s": "1 min/s",
"Rail": "Trens",
"Rail": "Ferroviário",
"Urban rail": "Urbano",
"Local rail": "Suburbano",
"Regional rail": "Regional",
@@ -3554,32 +3554,30 @@
"Hide {category}": "Ocultar {category}",
"Show {category}": "Mostrar {category}",
"Live GPS": "GPS ao vivo",
"Estimated from station reports": "Posição estimada pelos registros das estações",
"Estimated from station reports": "Estimada a partir dos registros das estações",
"To {headsign}": "Para {headsign}",
"Next {stop} {time} {relative}": "Próxima parada {stop} {time} {relative}",
"Prev {stop} {time} {relative}": "Parada anterior {stop} {time} {relative}",
"(+{minutes} min late)": "(+{minutes} min de atraso)",
"Exit cockpit to fly to a train": "Saia da cabine para voar até um trem",
"Photo © {photographer}": "Foto © {photographer}",
"Photo: {photographer} · {licence}": "Foto: {photographer} · {licence}",
"Photo · {licence}": "Foto · {licence}",
"The shared flight isn't reporting now — showing where it was last seen": "O voo compartilhado não está transmitindo agora — mostrando a última posição conhecida",
"The shared flight isn't reporting now — showing where it was at {time}": "O voo compartilhado não está transmitindo agora — mostrando onde estava em {time}",
"The shared flight isn't reporting now — showing where it was at {time}": "O voo compartilhado não está transmitindo agora — mostrando onde estava às {time}",
"The shared military flight isn't reporting now — showing where it was last seen": "O voo militar compartilhado não está transmitindo agora — mostrando a última posição conhecida",
"The shared military flight isn't reporting now — showing where it was at {time}": "O voo militar compartilhado não está transmitindo agora — mostrando onde estava em {time}",
"The shared military flight isn't reporting now — showing where it was at {time}": "O voo militar compartilhado não está transmitindo agora — mostrando onde estava às {time}",
"The shared vessel isn't reporting now — showing where it was last seen": "A embarcação compartilhada não está transmitindo agora — mostrando a última posição conhecida",
"The shared vessel isn't reporting now — showing where it was at {time}": "A embarcação compartilhada não está transmitindo agora — mostrando onde estava em {time}",
"The shared vessel isn't reporting now — showing where it was at {time}": "A embarcação compartilhada não está transmitindo agora — mostrando onde estava às {time}",
"The shared satellite isn't reporting now — showing where it was last seen": "O satélite compartilhado não está transmitindo agora — mostrando a última posição conhecida",
"The shared satellite isn't reporting now — showing where it was at {time}": "O satélite compartilhado não está transmitindo agora — mostrando onde estava em {time}",
"The shared satellite isn't reporting now — showing where it was at {time}": "O satélite compartilhado não está transmitindo agora — mostrando onde estava às {time}",
"ICAO: {id}": "ICAO: {id}",
"NORAD: {id}": "NORAD: {id}",
"MMSI: {id}": "MMSI: {id}",
"Vessel UUID: {id}": "UUID da embarcação: {id}",
"Latitude must be within ±90° and longitude within ±180°": "A latitude deve estar dentro de ±90° e a longitude dentro de ±180°",
"Street Level is unavailable": "A camada \"Nível da rua\" não está disponível",
"Street Level is unavailable": "Nível da rua não está disponível",
"No street-level imagery source is configured": "Nenhuma fonte de imagens no nível da rua configurada",
"Street Level could not be enabled": "Não foi possível ativar a camada \"Nível da rua\"",
"Street Level changed while it looked for imagery": "A camada \"Nível da rua\" mudou enquanto procurava imagens",
"Street Level could not be enabled": "Não foi possível ativar Nível da rua",
"Street Level changed while it looked for imagery": "Nível da rua mudou enquanto procurava imagens",
"Aim the view at a street": "Aponte a vista para uma rua",
"{source} is not a street-level imagery source here": "{source} não é uma fonte de imagens no nível da rua aqui",
"{source} needs its key": "{source} precisa da sua chave",
@@ -3591,11 +3589,11 @@
"No {source} imagery near {place}": "Nenhuma imagem de {source} perto de {place}",
"{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",
"zoom in to load this layer": "aproxime para carregar esta camada",
"Zoom to layer": "Ir para a camada",
"Zoom to layer · fly to where this layer has data": "Ir para a camada · voe até onde esta camada tem dados",
"Remove from Data Layers": "Remover de \"Camadas de dados\"",
"Remove from Data Layers · add it again from the library": "Remover de \"Camadas de dados\" · adicione de novo pela biblioteca",
"Remove from Data Layers": "Remover de Camadas de dados",
"Remove from Data Layers · add it again from the library": "Remover de Camadas de dados · adicione de novo pela biblioteca",
"Coverage outline": "Contorno de cobertura",
"DRAPED": "SOBREPOSTO",
"Below 250 m": "Abaixo de 250 m",
@@ -3669,28 +3667,28 @@
"DATA LIBRARY": "BIBLIOTECA DE DADOS",
"Close data library": "Fechar a biblioteca de dados",
"Data Library at a glance": "A biblioteca de dados em resumo",
"Live sources beyond the core layers. Each one is fetched from its provider while it is on; nothing is stored in this app. Add one and it appears in Data Layers under Library.": "Fontes ao vivo além das camadas principais. Cada uma é obtida do seu provedor enquanto está ativa; nada é armazenado neste app. Adicione uma e ela aparecerá em \"Camadas de dados\", em \"Biblioteca\".",
"Live sources beyond the core layers. Each one is fetched from its provider while it is on; nothing is stored in this app. Add one and it appears in Data Layers under Library.": "Fontes ao vivo além das camadas principais. Cada uma é obtida do seu provedor enquanto está ativa; nada é armazenado neste app. Adicione uma e ela aparecerá em Camadas de dados, em Biblioteca.",
"Name, provider or country": "Nome, provedor ou país",
"Theme": "Tema",
"All themes": "Todos os temas",
"Only what this instance serves": "Só o que esta instância oferece",
"OPT-IN": "ATIV. PELO OPERADOR",
"OPT-IN": "OPCIONAL",
"KEY": "CHAVE",
"KEYLESS": "SEM CHAVE",
"ZOOM": "APROXIMAR",
"Zoom to {name}": "Ir para {name}",
"WORKING…": "PROCESSANDO…",
"ADD": "ADICIONAR",
"Remove {name} from Data Layers": "Remover {name} de \"Camadas de dados\"",
"Add {name} to Data Layers": "Adicionar {name} a \"Camadas de dados\"",
"Remove {name} from Data Layers": "Remover {name} de Camadas de dados",
"Add {name} to Data Layers": "Adicionar {name} a Camadas de dados",
"CONFIRM · REMOVE ALL {count}": {
"one": "CONFIRMAR · REMOVER {count}",
"many": "CONFIRMAR · REMOVER TODAS AS {count}",
"other": "CONFIRMAR · REMOVER TODAS AS {count}"
},
"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",
"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}": "Mostrando {shown} de {total}",
"BROWSE DATA LIBRARY": "EXPLORAR BIBLIOTECA DE DADOS"
}
+86 -88
View File
@@ -1,6 +1,6 @@
{
"News feed unavailable": "Лента новостей недоступна",
"Developing: {name}": "Развитие событий: {name}",
"Developing: {name}": "Развивается: {name}",
"{count} events today": {
"one": "{count} событие сегодня",
"few": "{count} события сегодня",
@@ -19,12 +19,12 @@
"Claimed by {who}": "Заявление: {who}",
"Claimed": "Заявление",
"Not yet confirmed by others": "Пока не подтверждено другими",
"Reported by {side} sources": "Сообщают источники: {side}",
"Reported by {sides}": "Сообщают источники: {sides}",
"{side} sources": "{side}",
"Reported by {side} sources": "Сообщают источники {side}",
"Reported by {sides}": "Сообщают: {sides}",
"{side} sources": "источники {side}",
"Official": "Официально",
"Reported by both sides": "Сообщают обе стороны",
"Independently reported": "Сообщают независимые источники",
"Independently reported": "Подтверждено независимыми источниками",
"Single source": "Один источник",
"International": "Международные",
"Independent": "Независимые",
@@ -42,7 +42,7 @@
"Air defence": "ПВО",
"Ground fighting": "Наземные бои",
"Casualties": "Жертвы",
"Humanitarian": "Гуманитарная ситуация",
"Humanitarian": "Гуманитарное",
"Disaster": "Бедствие",
"Protest": "Протест",
"Political": "Политика",
@@ -50,7 +50,7 @@
"Minor incident": "Незначительный инцидент",
"Significant": "Значительный",
"Major": "Крупный",
"age|now": "только что",
"age|now": "сейчас",
"{minutes} min": "{minutes} мин",
"{hours} h": "{hours} ч",
"{count} sources": {
@@ -60,7 +60,7 @@
"other": "{count} источника"
},
"News feed not configured on this server": "Лента новостей не настроена на этом сервере",
"Malformed news hotspots response": "Некорректный ответ с горячими точками новостей",
"Malformed news hotspots response": "Некорректный ответ горячих точек новостей",
"Malformed news feed response": "Некорректный ответ ленты новостей",
"{place} (placed by model)": "{place} (место определила модель)",
"English headline by the model": "Английский заголовок от модели",
@@ -68,7 +68,7 @@
"Close": "Закрыть",
"Any severity": "Любая тяжесть",
"{severity} and above": "{severity} и выше",
"{severity} only": "Только: {severity}",
"{severity} only": "Только {severity}",
"All categories": "Все категории",
"Reading the news…": "Читаем новости…",
"News, last {hours} hours": "Новости за последние {hours} ч",
@@ -87,7 +87,7 @@
"Load older news": "Загрузить более ранние новости",
"How to read the map": "Как читать карту",
"Colour: severity · icon: type of event": "Цвет: тяжесть · значок: тип события",
"Solid rim: reported by both sides or independently. Dashed rim: a claim. Thin rim: one source, or one side’s own press.": "Сплошной контур: сообщают обе стороны или независимые источники. Пунктирный контур: заявление. Тонкий контур: один источник или пресса одной стороны.",
"Solid rim: reported by both sides or independently. Dashed rim: a claim. Thin rim: one source, or one side’s own press.": "Сплошной обод: сообщают обе стороны или независимые источники. Пунктирный обод: заявление. Тонкий обод: один источник или пресса одной стороны.",
"Larger: more sources · fainter: older. Picture card: a major or fast-growing event with two or more sources.": "Крупнее: больше источников · бледнее: старше. Карточка с фото: крупное или быстро растущее событие от двух и более источников.",
"Side labels describe the source, not whether a report is true.": "Метки сторон описывают источник, а не достоверность сообщения.",
"Exit cockpit to fly to a news report": "Выйдите из кабины, чтобы перелететь к сообщению",
@@ -268,7 +268,7 @@
"PAUSED · {title}": "ПАУЗА · {title}",
"UNSPECIFIED": "НЕ УКАЗАНО",
"UNAVAILABLE": "НЕДОСТУПНО",
"+{count} additional payload records": "Ещё записей о полезной нагрузке: {count}",
"+{count} additional payload records": "+{count} доп. записи о полезной нагрузке",
"PAYLOAD DATA UNAVAILABLE": "ДАННЫЕ ПОЛЕЗНОЙ НАГРУЗКИ НЕДОСТУПНЫ",
"{value} KM DOWNRANGE": "{value} КМ ПО ТРАССЕ",
"POSITION UNAVAILABLE": "ПОЗИЦИЯ НЕДОСТУПНА",
@@ -285,8 +285,8 @@
"{direction} {angle}°": "{direction} {angle}°",
"OFF-NADIR {angle}° · {offset}": "ОТКЛОНЕНИЕ ОТ НАДИРА {angle}° · {offset}",
"SITES STANDBY": "ОБЪЕКТЫ В ОЖИДАНИИ",
"ENABLE MAPPED INSTALLATIONS FOR SITE PINGS": "ВКЛЮЧИТЕ «ОБЪЕКТЫ НА КАРТЕ» ДЛЯ ПИНГОВ ОБЪЕКТОВ",
"Release the camera from {name}": "Отпустить камеру с объекта: {name}",
"ENABLE MAPPED INSTALLATIONS FOR SITE PINGS": "ВКЛЮЧИТЕ ОТМЕЧЕННЫЕ ОБЪЕКТЫ ДЛЯ ПИНГОВ МЕСТ",
"Release the camera from {name}": "Освободить камеру от {name}",
"RETURN · {name}": "НАЗАД · {name}",
"PREVIOUS SATELLITE": "ПРЕДЫДУЩИЙ СПУТНИК",
"Return to {name}": "Вернуться к {name}",
@@ -295,8 +295,8 @@
"Unnamed site": "Безымянный объект",
"OFF-NADIR {angle}°": "ОТКЛОНЕНИЕ ОТ НАДИРА {angle}°",
"FOCUS · {name}": "ФОКУС · {name}",
"Keep the camera on {name}": "Удерживать камеру на объекте: {name}",
"ZOOM {zoom}×": "МАСШТАБ {zoom}×",
"Keep the camera on {name}": "Удерживать камеру на {name}",
"ZOOM {zoom}×": "ПРИБЛИЖЕНИЕ {zoom}×",
"SWATH {value} KM": "ПОЛОСА ОБЗОРА {value} КМ",
"SWATH ≈ {value} KM": "ПОЛОСА ОБЗОРА ≈ {value} КМ",
"Orbital view": "Орбитальный вид",
@@ -304,7 +304,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}",
@@ -346,7 +346,7 @@
"Cancel replay": "Отменить повтор",
"EST. DOWNRANGE": "РАСЧЁТ ПО ТРАССЕ",
"CONFIRMED": "ПОДТВЕРЖДЕНО",
"PAD / RTLS": "ПЛОЩАДКА / RTLS",
"PAD / RTLS": "СТАРТОВАЯ ПЛОЩАДКА / RTLS",
"PLANNED · ": "ПЛАН · ",
"SUPPLIED TRAJECTORY POINTS": "ПРЕДОСТАВЛЕННЫЕ ТОЧКИ ТРАЕКТОРИИ",
"RECONSTRUCTED ESTIMATE": "РЕКОНСТРУИРОВАННАЯ ОЦЕНКА",
@@ -376,7 +376,7 @@
"Starlink {detail} — click to retry": "Starlink: {detail} — нажмите, чтобы повторить",
"load failed": "ошибка загрузки",
"Showing the full Starlink shell — click for the core catalog only": "Показана полная оболочка Starlink — нажмите, чтобы оставить только основной каталог",
"DENSE": "ПЛОТНО",
"DENSE": "ПЛОТНЫЙ",
"Syncing flow · Roads: {source}": "Синхронизация потока · Дороги: {source}",
"LIVE · Roads: {source} · Roads without flow hidden": "ОНЛАЙН · Дороги: {source} · Дороги без потока скрыты",
"LIVE · Roads: {source} · No flow roads in view": "ОНЛАЙН · Дороги: {source} · В кадре нет дорог с потоком",
@@ -469,10 +469,10 @@
"other": "Загружено {count} камеры · включите CCTV, чтобы активировать"
},
"{count} more cameras outside this view · pan or zoom out": {
"one": "Ещё {count} камера вне поля зрения · сдвиньте или отдалите карту",
"few": "Ещё {count} камеры вне поля зрения · сдвиньте или отдалите карту",
"many": "Ещё {count} камер вне поля зрения · сдвиньте или отдалите карту",
"other": "Ещё {count} камеры вне поля зрения · сдвиньте или отдалите карту"
"one": "Ещё {count} камера вне этого вида · сдвиньте или отдалите",
"few": "Ещё {count} камеры вне этого вида · сдвиньте или отдалите",
"many": "Ещё {count} камер вне этого вида · сдвиньте или отдалите",
"other": "Ещё {count} камеры вне этого вида · сдвиньте или отдалите"
},
"Enable CCTV to load camera intersections": "Включите CCTV, чтобы загрузить камеры на перекрёстках",
"Enable CCTV to start camera-linked intelligence summaries.": "Включите CCTV, чтобы запускать сводки по камерам.",
@@ -545,7 +545,7 @@
"ROUTE DATA UNAVAILABLE": "ДАННЫЕ МАРШРУТА НЕДОСТУПНЫ",
"CONTACT NAVIGATION OFF": "НАВИГАЦИЯ ПО КОНТАКТАМ ВЫКЛЮЧЕНА",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "МОСТИК ОСТАЁТСЯ НА ВЫБРАННОМ СУДНЕ",
"{range} NM": "{range} МОР. МИЛ.",
"{range} NM": "{range} МОР. МИЛЬ",
"AIS DESTINATION · CREW REPORTED": "ПУНКТ НАЗНАЧЕНИЯ AIS · УКАЗАН ЭКИПАЖЕМ",
"DEST {bearing}": "НАЗН. {bearing}",
"Disable cockpit weather effects": "Выключить погодные эффекты кабины",
@@ -742,7 +742,7 @@
"ACQUIRING": "ЗАХВАТ",
"SHARED {subject}": "ПО ССЫЛКЕ: {subject}",
"entity": "объект",
"Shared {subject} follow expired": "Срок отслеживания по ссылке истёк: {subject}",
"Shared {subject} follow expired": "Срок отслеживания {subject} по ссылке истёк",
"Shared {subject} could not be restored — feed unavailable": "Не удалось восстановить {subject} по ссылке — поток данных недоступен",
"Shared {subject} is unavailable": "{subject} по ссылке недоступен",
"LOADING LIVE DATA": "ЗАГРУЗКА ОНЛАЙН-ДАННЫХ",
@@ -804,8 +804,8 @@
"Live signals": "Сигналы в реальном времени",
"Latest regional news": "Последние региональные новости",
"Location-based information": "Информация по текущему месту",
"LOOK STBD {bearing}°": "ВЗГЛЯД НА ПР. БОРТ {bearing}°",
"LOOK PORT {bearing}°": "ВЗГЛЯД НА ЛЕВ. БОРТ {bearing}°",
"LOOK STBD {bearing}°": "ВЗГЛЯД ПР. БОРТ {bearing}°",
"LOOK PORT {bearing}°": "ВЗГЛЯД ЛЕВ. БОРТ {bearing}°",
"LOOK ASTERN": "ВЗГЛЯД ЗА КОРМУ",
"UP {angle}°": "ВВЕРХ {angle}°",
"DOWN {angle}°": "ВНИЗ {angle}°",
@@ -823,7 +823,7 @@
"LIVE TRACK · COURSE ALIGNED": "ТРЕК ОНЛАЙН · КУРС СОВМЕЩЁН",
"GROUND SPEED": "ПУТЕВАЯ СКОРОСТЬ",
"SPEED OVER GROUND": "СКОРОСТЬ ПО ГРУНТУ",
"COURSE OVER GROUND": "ПУТЕВОЙ УГОЛ",
"COURSE OVER GROUND": "КУРС ПО ГРУНТУ",
"ALTITUDE": "ВЫСОТА",
"CONTEXT ONLY": "ТОЛЬКО КОНТЕКСТ",
"AVAILABLE INPUTS ONLY · NOT AN ALL-CLEAR": "ТОЛЬКО ДОСТУПНЫЕ ДАННЫЕ · ОТСУТСТВИЕ УГРОЗ НЕ ПОДТВЕРЖДЕНО",
@@ -842,14 +842,14 @@
"EXIT BRIDGE": "ВЫЙТИ С МОСТИКА",
"EXIT ORBIT": "ВЫЙТИ С ОРБИТЫ",
"CONTACTS · 250 KM": "КОНТАКТЫ · 250 КМ",
"RINGS {spacing} NM": "КОЛЬЦА {spacing} МОР. МИЛ.",
"RINGS {spacing} NM": "КОЛЬЦА {spacing} МОР. МИЛЬ",
"{count} CONTACTS": {
"one": "{count} КОНТАКТ",
"few": "{count} КОНТАКТА",
"many": "{count} КОНТАКТОВ",
"other": "{count} КОНТАКТА"
},
"Radar range {range}. Select for the next range.": "Шкала дальности радара {range} — выберите, чтобы перейти к следующей шкале.",
"Radar range {range}. Select for the next range.": "Шкала дальности радара {range}. Выберите, чтобы перейти к следующей шкале.",
"Bridge radar": "Радар ходового мостика",
"Expand Visual Presets": "Развернуть визуальные пресеты",
"Pin visual presets": "Закрепить визуальные пресеты",
@@ -970,7 +970,7 @@
"DEL": "УДАЛ.",
"UPDATE SHOT": "ОБНОВИТЬ КАДР",
"IMPORT": "ИМПОРТ",
"RUN LOG": "ЖУРНАЛ ТАЙМИНГОВ",
"RUN LOG": "ЖУРНАЛ ЗАПУСКОВ",
"CALIBRATION": "КАЛИБРОВКА",
"Camera pose — click a value to type": "Поза камеры — щёлкните по значению, чтобы ввести",
"SCENE SUMMARY": "СВОДКА ПО СЦЕНЕ",
@@ -2131,9 +2131,9 @@
"N/A": "Н/Д",
"{label} · {radius} AIR/SEA WINDOW": "{label} · ОКНО ВОЗДУХ/МОРЕ {radius}",
"{scope} · INSTALLATIONS {coverage}": "{scope} · ОБЪЕКТЫ: {coverage}",
"MILITARY FEED OFF · PARTIAL PICTURE": "ПОТОК ВОЕННЫХ РЕЙСОВ ВЫКЛ · КАРТИНА НЕПОЛНАЯ",
"MILITARY FEED OFF · PARTIAL PICTURE": "ВОЕННЫЙ ПОТОК ВЫКЛ · КАРТИНА НЕПОЛНАЯ",
"FLIGHTS FEED OFF · PARTIAL PICTURE": "ПОТОК РЕЙСОВ ВЫКЛ · КАРТИНА НЕПОЛНАЯ",
"NEARBY AIRCRAFT INCOMPLETE · NOT AN ALL-CLEAR": "БЛИЖАЙШИЕ САМОЛЁТЫ: ДАННЫЕ НЕПОЛНЫЕ · ОТСУТСТВИЕ УГРОЗ НЕ ПОДТВЕРЖДЕНО",
"NEARBY AIRCRAFT INCOMPLETE · NOT AN ALL-CLEAR": "БЛИЖНИЕ ВОЗДУШНЫЕ СУДА: ДАННЫЕ НЕПОЛНЫЕ · ОТСУТСТВИЕ УГРОЗ НЕ ПОДТВЕРЖДЕНО",
"{source} · LAST {hours} H": "{source} · ЗА {hours} Ч",
"ft": "фт",
"kts": "уз",
@@ -2416,8 +2416,8 @@
"Import": "Импорт",
"Choose GeoJSON, KML, KMZ or GPX files": "Выберите файлы GeoJSON, KML, KMZ или GPX",
"Imported files": "Импортированные файлы",
"DOCKED · {name}": "ПРИСТЫКОВАН · {name}",
"DOCKED · {name} · +{extra}": "ПРИСТЫКОВАНЫ · {name} · +{extra}",
"DOCKED · {name}": "СТЫКОВКА · {name}",
"DOCKED · {name} · +{extra}": "СТЫКОВКА · {name} · +{extra}",
"loading coverage...": "загрузка покрытия...",
"live feed unavailable": "поток в реальном времени недоступен",
"GBFS fetch error": "ошибка загрузки GBFS",
@@ -2936,7 +2936,7 @@
"Hybrid": "Гибрид",
"Lines": "Линии",
"Dots": "Точки",
"Road lines colored by live flow: green to red": "Линии дорог, окрашенные по потоку онлайн: от зелёного к красному",
"Road lines colored by live flow: green to red": "Линии дорог по потоку онлайн: от зелёного к красному",
"Moving dots colored by live flow": "Движущиеся точки, окрашенные по потоку онлайн",
"Moving dots": "Движущиеся точки",
"TomTom roads only, each with its live flow": "Только дороги TomTom, у каждой — свой поток онлайн",
@@ -3183,11 +3183,11 @@
"Gemini Live connection is expiring. Start voice again for a new conversation.": "Соединение с Gemini Live истекает. Запустите голосовой режим заново для нового разговора.",
"Gemini voice sessions have a time limit. When a session expires, start voice again for a new conversation.": "Время голосовых сеансов Gemini ограничено. Когда сеанс истечёт, запустите голосовой режим заново для нового разговора.",
"Gemini Live voice control": "Голосовое управление через Gemini Live",
"Voice provider": "Голосовой провайдер",
"Changing provider reconnects voice if it is on.": "Смена провайдера переподключает голосовой режим, если он включён.",
"Voice provider": "Поставщик голосового сервиса",
"Changing provider reconnects voice if it is on.": "Смена поставщика переподключает голосовой режим, если он включён.",
"Gemini voice could not connect. Try again.": "Не удалось подключить голосовой режим Gemini. Повторите попытку.",
"Gemini voice requires a same-origin app request": "Голосовой режим Gemini требует запрос из того же источника, что и приложение",
"Add a Gemini API key in Provider Settings": "Добавьте ключ API Gemini в «Настройках провайдеров»",
"Add a Gemini API key in Provider Settings": "Добавьте ключ API Gemini в настройках поставщиков",
"The Gemini Live model configuration is invalid": "Недопустимая конфигурация модели Gemini Live",
"Gemini voice is starting too often. Try again shortly.": "Голосовой режим Gemini запускается слишком часто. Повторите попытку чуть позже.",
"Gemini quota is unavailable. Try again later.": "Квота Gemini недоступна. Повторите попытку позже.",
@@ -3215,7 +3215,7 @@
"S2 · {name}": "S2 · {name}",
"Report": "Сводка",
"Open the source for {title}": "Открыть источник: {title}",
"LIVE · GitHub {commit} · {message} · checked every {seconds}s": "ОНЛАЙН · GitHub {commit} · {message} · проверка каждые {seconds} с",
"LIVE · GitHub {commit} · {message} · checked every {seconds}s": "В ЭФИРЕ · GitHub {commit} · {message} · проверка каждые {seconds} с",
"GitHub {commit} · {message} · reconnecting for live updates": "GitHub {commit} · {message} · переподключение для обновлений в реальном времени",
"Connecting to the S2 GhostMaps repository…": "Подключение к репозиторию S2 GhostMaps…",
"{count} placemarks": {
@@ -3292,7 +3292,7 @@
"Click a shot to select it, double-click to rename.": "Щёлкните кадр, чтобы выбрать его, дважды — чтобы переименовать.",
"scene|PLAY": "ПУСК",
"Stop playback (Esc)": "Остановить воспроизведение (Esc)",
"SAVE & SHARE": "СОХРАНЕНИЕ И ОБМЕН",
"SAVE & SHARE": "СОХРАНИТЬ И ПОДЕЛИТЬСЯ",
"Download every scene as a JSON file you can import later": "Скачать все сцены файлом JSON для последующего импорта",
"Load scenes from an exported JSON file": "Загрузить сцены из экспортированного файла JSON",
"Download the timing log of the last play": "Скачать журнал таймингов последнего воспроизведения",
@@ -3303,7 +3303,7 @@
"Left panels": "Левые панели",
"Right panels": "Правые панели",
"UTILITIES": "ИНСТРУМЕНТЫ",
"Exit cockpit to fly to a drawing": "Выйдите из кабины, чтобы перелететь к фигуре",
"Exit cockpit to fly to a drawing": "Выйдите из кабины, чтобы перелететь к рисунку",
"Directions, drawing and your own files": "Маршруты, рисование и ваши файлы",
"Voice place search (needs an Overpass server)": "Голосовой поиск мест (нужен сервер Overpass)",
"OSM Places": "Места OSM",
@@ -3458,14 +3458,14 @@
"Within {km} km of the view centre": "В радиусе {km} км от центра карты",
"{region}, {country}": "{region}, {country}",
"via {source}": "через {source}",
"Local in the News layer: the day’s news from the outlets of the country in view": "Раздел «Местные» в слое «Новости»: новости дня от СМИ страны в кадре",
"Local in the News layer: the day’s news from the outlets of the country in view": "Местное в слое новостей: новости дня от СМИ страны в кадре",
"{provider} refused its key": "{provider} отклонил ключ",
"Check {envVars} in Provider Settings": "Проверьте {envVars} в «Настройках провайдеров»",
"{provider} key required": "Нужен ключ {provider}",
"{provider} limit reached for today": "{provider}: дневной лимит исчерпан",
"News resumes tomorrow, or with a paid plan": "Новости вернутся завтра или с платным тарифом",
"News key required": "Нужен ключ новостей",
"Precise conflict points in the News layer, beside GDELT’s articles": "Точные координаты конфликтов в слое «Новости», в дополнение к статьям GDELT",
"Precise conflict points in the News layer, beside GDELT’s articles": "Точные точки конфликтов в слое новостей, вдобавок к статьям GDELT",
"Worldwide": "Весь мир",
"Shipwrecks": "Кораблекрушения",
"Before 1900": "До 1900",
@@ -3480,7 +3480,7 @@
"Depth: {m} m": "Глубина: {m} м",
"Notes: {value}": "Примечания: {value}",
"Cargo or finds: {value}": "Груз или находки: {value}",
"Protected wreck": "Затонувшее судно под охраной",
"Protected wreck": "Охраняемый объект",
"Source: {value}": "Источник: {value}",
"Click the card to open EMODnet ↗": "Нажмите на карточку, чтобы открыть EMODnet ↗",
"Click the card to open the source ↗": "Нажмите на карточку, чтобы открыть источник ↗",
@@ -3511,9 +3511,9 @@
"seen {age} ago": "замечено {age} назад",
"observed {age} ago": "наблюдалось {age} назад",
"Be Whale Wise": "Наблюдайте китов бережно",
"Reported by {name} ({entity})": "Источник: {name} ({entity})",
"Reported by {name}": "Источник: {name}",
"Salish Sea": "Море Селиш",
"Reported by {name} ({entity})": "Сообщил: {name} ({entity})",
"Reported by {name}": "Сообщил: {name}",
"Salish Sea": "Селишское море",
"Birds": "Птицы",
"Mammals": "Млекопитающие",
"Reptiles": "Пресмыкающиеся",
@@ -3528,8 +3528,8 @@
"{label} {detail} — click to retry": "{label} {detail} — нажмите, чтобы повторить",
"Wildlife sightings source unavailable": "Источник наблюдений живой природы недоступен",
"Show only community-confirmed (research-grade) sightings": "Показать только подтверждённые сообществом наблюдения исследовательского уровня",
"No recent sightings in view": "В кадре нет недавних наблюдений",
"Acartia feed unavailable": "Поток Acartia недоступен",
"No recent sightings in view": "Нет недавних наблюдений в области обзора",
"Acartia feed unavailable": "Лента Acartia недоступна",
"Show all taxa": "Показать все группы организмов",
"BIRDS": "ПТИЦЫ",
"Show only birds": "Показать только птиц",
@@ -3544,31 +3544,31 @@
"All rights reserved": "Все права защищены",
"Cetacean": "Китообразное",
"Wildlife Sightings": "Наблюдения живой природы",
"Exit cockpit to fly to a sighting": "Выйдите из кабины, чтобы перелететь к наблюдению",
"Exit cockpit to fly to a sighting area": "Выйдите из кабины, чтобы перелететь к области наблюдения",
"Exit cockpit to fly to a sighting": "Выйдите из кабины, чтобы перейти к наблюдению",
"Exit cockpit to fly to a sighting area": "Выйдите из кабины, чтобы перейти к области наблюдения",
"Wildlife": "Живая природа",
"Zoom in for local sightings": "Приблизьте карту, чтобы увидеть местные наблюдения",
"Zoom in for local sightings": "Приблизьте для местных наблюдений",
"Loading photo...": "Загрузка фото…",
"Photo unavailable": "Фото недоступно",
"Location obscured to a 0.2° cell": "Местоположение скрыто в ячейке 0,2°",
"Photo of {name}": "Фото: {name}",
"Photo of {name}": "Фото {name}",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": "Наблюдений в кадре: {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°. Это не учёт численности.",
"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",
"Open {name} on Acartia": "Открыть {name} в Acartia",
"iNaturalist": "iNaturalist",
"iNaturalist contributors · licences per record": "Участники iNaturalist · лицензия у каждой записи",
"iNaturalist contributors · Acartia {count} (Salish Sea) · licences per record": "Участники iNaturalist · Acartia {count} (море Селиш) · лицензия у каждой записи",
"{days}d": "{days} дн.",
"iNaturalist contributors · Acartia {count} (Salish Sea) · licences per record": "Участники iNaturalist · Acartia {count} (море Сэлиш) · лицензия у каждой записи",
"{days}d": "{days}д",
"All markers": "Все метки",
"Show markers of every age": "Показать метки любого возраста",
"Maximum event age: {days}d; undated markers are kept": "Максимальный возраст события: {days} дн.; метки без даты остаются",
"Maximum event age: {days}d; undated markers are kept": "Максимальный возраст события: {days} дн.; метки без даты сохранены",
"event age filter|All": "Все",
"Event age": "Возраст события",
"Event age: {days}d · undated kept": "Возраст события: {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.": "Кадр на карте запрашивается отдельно и может показывать другой момент.",
@@ -3596,18 +3596,18 @@
"Space science, technology demonstration and other payloads": "Космическая наука, демонстрация технологий и другие полезные нагрузки",
"DEBRIS": "МУСОР",
"Rocket bodies, fragments and dead satellites": "Ступени ракет, обломки и неработающие спутники",
"Classes may lag: SATCAT is a cached copy": "Классы могут быть неактуальны: SATCAT — кэшированная копия",
"Classes may lag: SATCAT unavailable": "Классы могут быть неактуальны: SATCAT недоступен",
"Classes may lag: SATCAT is a cached copy": "Классы могут отставать: SATCAT — кэшированная копия",
"Classes may lag: SATCAT unavailable": "Классы могут отставать: SATCAT недоступен",
"CelesTrak SATCAT gives each object's type and status. Without a current copy the classes rest on the UCS database, names and CelesTrak groups, so a satellite that has died since may still show as working.": "CelesTrak SATCAT сообщает тип и состояние каждого объекта. Без актуальной копии классы опираются на базу UCS, названия и группы CelesTrak, поэтому вышедший с тех пор из строя спутник может по-прежнему показываться работающим.",
"STARLINK ···": "STARLINK ···",
"STARLINK ✕": "STARLINK ✕",
"SENSOR SWATH {value} KM": "ПОЛОСА ЗАХВАТА {value} КМ",
"SENSOR SWATH ≈ {value} KM": "ПОЛОСА ЗАХВАТА ≈ {value} КМ",
"SENSOR SWATH {value} KM": "ПОЛОСА СЕНСОРА {value} КМ",
"SENSOR SWATH ≈ {value} KM": "ПОЛОСА СЕНСОРА ≈ {value} КМ",
"SENSOR": "СЕНСОР",
"Zoom to the imaging sensor swath": "Приблизить до полосы захвата сенсора",
"Zoom to the imaging sensor swath": "Приблизить до полосы съёмочного сенсора",
"<1 H OLD": "<1 Ч НАЗАД",
"{value} H OLD": "{value} Ч НАЗАД",
"{value} D OLD": "{value} ДН. НАЗАД",
"{value} D OLD": "{value} Д НАЗАД",
"FWD": "ВПЕРЁД",
"AFT": "НАЗАД",
"RIGHT": "ВПРАВО",
@@ -3618,11 +3618,11 @@
"Zoom out (−)": "Отдалить (−)",
"ORBITAL VIEW · OFF-NADIR": "ОРБИТАЛЬНЫЙ ВИД · ОТКЛОНЕНИЕ ОТ НАДИРА",
"World time": "Мировое время",
"Back to live": "Вернуться к текущему времени",
"Back to live": "Вернуться к прямому эфиру",
"Live": "Сейчас",
"1 h/s": "1 ч/с",
"1 d/s": "1 дн./с",
"1 wk/s": "1 нед./с",
"1 d/s": "1 дн/с",
"1 wk/s": "1 нед/с",
"Play speed": "Скорость воспроизведения",
"Scroll or pinch to zoom, drag to move along · double-click to see it all": "Прокрутите или сведите пальцы для масштаба, перетащите для перемещения · двойной щелчок — показать всё",
"1 min/s": "1 мин/с",
@@ -3637,31 +3637,29 @@
"Live GPS": "GPS в реальном времени",
"Estimated from station reports": "Оценка по отметкам станций",
"To {headsign}": "До {headsign}",
"Next {stop} {time} {relative}": "Следующая: {stop} {time} {relative}",
"Prev {stop} {time} {relative}": "Предыдущая: {stop} {time} {relative}",
"Next {stop} {time} {relative}": "Следующая {stop} {time} {relative}",
"Prev {stop} {time} {relative}": "Предыдущая {stop} {time} {relative}",
"(+{minutes} min late)": "(+{minutes} мин опоздания)",
"Exit cockpit to fly to a train": "Выйдите из кабины, чтобы перелететь к поезду",
"Photo © {photographer}": "Фото © {photographer}",
"Photo: {photographer} · {licence}": "Фото: {photographer} · {licence}",
"Photo · {licence}": "Фото · {licence}",
"The shared flight isn't reporting now — showing where it was last seen": "Рейс по ссылке сейчас не передаёт данные — показано последнее известное положение",
"The shared flight isn't reporting now — showing where it was at {time}": "Рейс по ссылке сейчас не передаёт данные — показано положение на {time}",
"The shared military flight isn't reporting now — showing where it was last seen": "Военный рейс по ссылке сейчас не передаёт данные — показано последнее известное положение",
"The shared military flight isn't reporting now — showing where it was at {time}": "Военный рейс по ссылке сейчас не передаёт данные — показано положение на {time}",
"The shared vessel isn't reporting now — showing where it was last seen": "Судно по ссылке сейчас не передаёт данные — показано последнее известное положение",
"The shared vessel isn't reporting now — showing where it was at {time}": "Судно по ссылке сейчас не передаёт данные — показано положение на {time}",
"The shared satellite isn't reporting now — showing where it was last seen": "Спутник по ссылке сейчас не передаёт данные — показано последнее известное положение",
"The shared satellite isn't reporting now — showing where it was at {time}": "Спутник по ссылке сейчас не передаёт данные — показано положение на {time}",
"The shared flight isn't reporting now — showing where it was last seen": "Общий рейс сейчас не передаёт данные — показано последнее известное место",
"The shared flight isn't reporting now — showing where it was at {time}": "Общий рейс сейчас не передаёт данные — показано место в {time}",
"The shared military flight isn't reporting now — showing where it was last seen": "Общий военный рейс сейчас не передаёт данные — показано последнее известное место",
"The shared military flight isn't reporting now — showing where it was at {time}": "Общий военный рейс сейчас не передаёт данные — показано место в {time}",
"The shared vessel isn't reporting now — showing where it was last seen": "Общее судно сейчас не передаёт данные — показано последнее известное место",
"The shared vessel isn't reporting now — showing where it was at {time}": "Общее судно сейчас не передаёт данные — показано место в {time}",
"The shared satellite isn't reporting now — showing where it was last seen": "Общий спутник сейчас не передаёт данные — показано последнее известное место",
"The shared satellite isn't reporting now — showing where it was at {time}": "Общий спутник сейчас не передаёт данные — показано место в {time}",
"ICAO: {id}": "ICAO: {id}",
"NORAD: {id}": "NORAD: {id}",
"MMSI: {id}": "MMSI: {id}",
"Vessel UUID: {id}": "UUID судна: {id}",
"Latitude must be within ±90° and longitude within ±180°": "Широта должна быть в пределах ±90°, а долгота — в пределах ±180°",
"Street Level is unavailable": "Слой «Уличные снимки» недоступен",
"Street Level is unavailable": "Уличные снимки недоступны",
"No street-level imagery source is configured": "Источник уличных снимков не настроен",
"Street Level could not be enabled": "Не удалось включить «Уличные снимки»",
"Street Level changed while it looked for imagery": "Состояние слоя «Уличные снимки» изменилось во время поиска",
"Aim the view at a street": "Наведите центр вида на улицу",
"Street Level could not be enabled": "Не удалось включить уличные снимки",
"Street Level changed while it looked for imagery": "Уличные снимки изменились во время поиска",
"Aim the view at a street": "Наведите вид на улицу",
"{source} is not a street-level imagery source here": "{source} здесь не является источником уличных снимков",
"{source} needs its key": "{source}: нужен ключ",
"{source} needs the {map} map, which is not available": "{source}: нужна карта {map}, но она недоступна",
@@ -3669,7 +3667,7 @@
"{source} is not available on this map": "{source}: недоступно на этой карте",
"{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 {place}": "Нет снимков {source} рядом с {place}",
"{reason} (filters on: {filters})": "{reason} (включены фильтры: {filters})",
"tiles unavailable": "тайлы недоступны",
"Environment": "Окружающая среда",
@@ -3688,15 +3686,15 @@
"Needs a provider key on the server": "Нужен ключ провайдера на сервере",
"Needs this app's server · not available in a static build": "Нужен сервер приложения · недоступно в статической сборке",
"Checking what this instance serves…": "Проверяем, что доступно на этом экземпляре…",
"Regional": "Регионально",
"Regional": "Региональный",
"DATA LIBRARY": "БИБЛИОТЕКА ДАННЫХ",
"Close data library": "Закрыть библиотеку данных",
"Live sources beyond the core layers. Each one is fetched from its provider while it is on; nothing is stored in this app. Add one and it appears in Data Layers under Library.": "Источники в реальном времени помимо основных слоёв. Каждый загружается у своего провайдера, пока он включён; приложение ничего не хранит. Добавьте источник, и он появится в «Слоях данных» в разделе «Библиотека».",
"Live sources beyond the core layers. Each one is fetched from its provider while it is on; nothing is stored in this app. Add one and it appears in Data Layers under Library.": "Живые источники помимо основных слоёв. Каждый загружается у своего провайдера, пока он включён; приложение ничего не хранит. Добавьте источник, и он появится в «Слоях данных» в разделе «Библиотека».",
"Name, provider or country": "Название, провайдер или страна",
"Theme": "Тема",
"All themes": "Все темы",
"Only what this instance serves": "Только то, что доступно на этом экземпляре",
"OPT-IN": "ВКЛЮЧАЕТ ОПЕРАТОР",
"OPT-IN": "ПО ВЫБОРУ",
"KEY": "КЛЮЧ",
"KEYLESS": "БЕЗ КЛЮЧА",
"WORKING…": "ВЫПОЛНЯЕТСЯ…",
+12 -14
View File
@@ -3328,7 +3328,7 @@
"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",
"Press OPEN to load the photo": "Tryck på ÖPPNA för att läsa in bilden",
"Captured {month}": "Tagen {month}",
"this spot": "den här platsen",
"the view centre": "vyns mitt",
@@ -3336,7 +3336,7 @@
"Street level at {place}": "Gatunivå vid {place}",
"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",
"Street View is ready: press {open} to load it": "Street View är redo: tryck på {open} för att läsa in den",
"{source} cannot open this link's street-level photo without a working key": "{source} kan inte öppna länkens bild på gatunivå utan en fungerande nyckel",
"Street-level photos are hidden in the current view. Leave it first.": "Bilder på gatunivå är dolda i den aktuella vyn. Lämna den först.",
"STATIONS": "STATIONER",
@@ -3457,7 +3457,7 @@
"REPORTED VOYAGE": "ANGIVEN RESA",
"1 min/s": "1 min/s",
"Rail": "Järnväg",
"Urban rail": "Tunnelbana/spårväg",
"Urban rail": "Stadstrafik",
"Local rail": "Lokaltåg",
"Regional rail": "Regionaltåg",
"National rail": "Fjärrtåg",
@@ -3465,15 +3465,13 @@
"Hide {category}": "Dölj {category}",
"Show {category}": "Visa {category}",
"Live GPS": "Live-GPS",
"Estimated from station reports": "Uppskattad utifrån stationsrapporter",
"Estimated from station reports": "Uppskattad från stationsrapporter",
"To {headsign}": "Mot {headsign}",
"Next {stop} {time} {relative}": "Nästa stopp {stop} {time} {relative}",
"Prev {stop} {time} {relative}": "Föregående stopp {stop} {time} {relative}",
"(+{minutes} min late)": "(+{minutes} min försening)",
"(+{minutes} min late)": "(+{minutes} min försenad)",
"Exit cockpit to fly to a train": "Lämna cockpiten för att flyga till ett tåg",
"Photo © {photographer}": "Foto © {photographer}",
"Photo: {photographer} · {licence}": "Foto: {photographer} · {licence}",
"Photo · {licence}": "Foto · {licence}",
"The shared flight isn't reporting now — showing where it was last seen": "Den delade flygningen rapporterar inte just nu – visar senast kända position",
"The shared flight isn't reporting now — showing where it was at {time}": "Den delade flygningen rapporterar inte just nu – visar positionen från {time}",
"The shared military flight isn't reporting now — showing where it was last seen": "Den delade militärflygningen rapporterar inte just nu – visar senast kända position",
@@ -3532,7 +3530,7 @@
"TILES": "KARTRUTOR",
"PACK": "PAKET",
"INCUBATING": "INKUBERAS",
"COMMUNITY": "ANVÄNDARBIDRAG",
"COMMUNITY": "GEMENSKAP",
"VERIFIED": "VERIFIERAD",
"ARCHIVED": "ARKIVERAD",
"Datasets": "Datamängder",
@@ -3563,11 +3561,11 @@
},
"switch on a dataset to load features": "slå på en datamängd för att ladda objekt",
"Switched on by this instance · part of the CCTV layer": "Påslaget av den här instansen · del av CCTV-lagret",
"Operator opt-in: set {env}=1 on the server": "Aktiveras av administratören: sätt {env}=1 på servern",
"Operator opt-in: add {id} to GEV_LIBRARY_ENABLE on the server": "Aktiveras av administratören: lägg till {id} i GEV_LIBRARY_ENABLE på servern",
"Turned off by this instance's operator": "Avstängt av instansens administratör",
"Operator opt-in: set {env}=1 on the server": "Aktiveras av operatören: sätt {env}=1 på servern",
"Operator opt-in: add {id} to GEV_LIBRARY_ENABLE on the server": "Aktiveras av operatören: lägg till {id} i GEV_LIBRARY_ENABLE på servern",
"Turned off by this instance's operator": "Avstängt av instansens operatör",
"Needs a provider key on the server": "Kräver en leverantörsnyckel på servern",
"Needs this app's server · not available in a static build": "Kräver appens server · inte tillgängligt i en statisk version",
"Needs this app's server · not available in a static build": "Kräver appens server · inte tillgängligt i en statisk build",
"Checking what this instance serves…": "Kontrollerar vad instansen tillhandahåller …",
"Regional": "Regionalt",
"Orthophoto": "Ortofoto",
@@ -3577,7 +3575,7 @@
"True orthophoto": "Sant ortofoto",
"Type": "Typ",
"Resolution": "Upplösning",
"Flown": "Fotograferat",
"Flown": "Flugen",
"Updates": "Uppdateras",
"Ortho quality": "Ortokvalitet",
"DATA LIBRARY": "DATABIBLIOTEK",
@@ -3588,7 +3586,7 @@
"Theme": "Tema",
"All themes": "Alla teman",
"Only what this instance serves": "Bara det instansen tillhandahåller",
"OPT-IN": "TILLVAL",
"OPT-IN": "VALFRITT",
"KEY": "NYCKEL",
"KEYLESS": "UTAN NYCKEL",
"ZOOM": "ZOOMA",
+110 -112
View File
@@ -1,6 +1,6 @@
{
"News feed unavailable": "Стрічка новин недоступна",
"Developing: {name}": "Розвиток подій: {name}",
"Developing: {name}": "Розвивається: {name}",
"{count} events today": {
"one": "{count} подія сьогодні",
"few": "{count} події сьогодні",
@@ -19,17 +19,17 @@
"Claimed by {who}": "Заява: {who}",
"Claimed": "Заява",
"Not yet confirmed by others": "Поки не підтверджено іншими",
"Reported by {side} sources": "Повідомляють джерела: {side}",
"Reported by {sides}": "Повідомляють джерела: {sides}",
"{side} sources": "{side}",
"Reported by {side} sources": "Повідомляють джерела {side}",
"Reported by {sides}": "Повідомляють: {sides}",
"{side} sources": "джерела {side}",
"Official": "Офіційно",
"Reported by both sides": "Повідомляють обидві сторони",
"Independently reported": "Повідомляють незалежні джерела",
"Independently reported": "Підтверджено незалежними джерелами",
"Single source": "Одне джерело",
"International": "Міжнародні",
"Independent": "Незалежні",
"Unattributed": "Без атрибуції",
"Outside party": "Третя сторона",
"Outside party": "Зовнішня сторона",
"Ukraine–Russia": "Україна–Росія",
"Israel–Gaza–Lebanon": "Ізраїль–Газа–Ліван",
"Iran–Hormuz": "Іран–Ормуз",
@@ -42,7 +42,7 @@
"Air defence": "ППО",
"Ground fighting": "Наземні бої",
"Casualties": "Жертви",
"Humanitarian": "Гуманітарна ситуація",
"Humanitarian": "Гуманітарне",
"Disaster": "Лихо",
"Protest": "Протест",
"Political": "Політика",
@@ -60,38 +60,38 @@
"other": "{count} джерела"
},
"News feed not configured on this server": "Стрічку новин не налаштовано на цьому сервері",
"Malformed news hotspots response": "Некоректна відповідь із гарячими точками новин",
"Malformed news hotspots response": "Некоректна відповідь гарячих точок новин",
"Malformed news feed response": "Некоректна відповідь стрічки новин",
"{place} (placed by model)": "{place} (місце визначила модель)",
"English headline by the model": "Англійський заголовок від моделі",
"Not placed on the map": "Не позначено на карті",
"Not placed on the map": "Не позначено на мапі",
"Close": "Закрити",
"Any severity": "Будь-яка тяжкість",
"{severity} and above": "{severity} і вище",
"{severity} only": "Лише {severity}",
"All categories": "Усі категорії",
"Reading the news…": "Читаємо новини…",
"News, last {hours} hours": "Новини за {hours} год",
"News, last {hours} hours": "Новини за останні {hours} год",
"Last {hours} h": "За {hours} год",
"Time window": "Період",
"Conflicts": "Конфлікти",
"Local": "Місцеві",
"News sections": "Розділи новин",
"Zoom in to see local news": "Наблизьте карту, щоб побачити місцеві новини",
"Zoom in to see local news": "Наблизьте мапу, щоб побачити місцеві новини",
"Hotspots": "Гарячі точки",
"Severity": "Тяжкість",
"Category": "Категорія",
"Nothing reported near here in the last {hours} h": "За {hours} год поблизу нічого не повідомлялося",
"Nothing reported near here in the last {hours} h": "За останні {hours} год поблизу нічого не повідомлялося",
"Latest news": "Останні новини",
"No news matches these filters in the last {hours} h": "За {hours} год немає новин за цими фільтрами",
"No news matches these filters in the last {hours} h": "За останні {hours} год немає новин за цими фільтрами",
"Load older news": "Завантажити давніші новини",
"How to read the map": "Як читати карту",
"How to read the map": "Як читати мапу",
"Colour: severity · icon: type of event": "Колір: тяжкість · значок: тип події",
"Solid rim: reported by both sides or independently. Dashed rim: a claim. Thin rim: one source, or one side’s own press.": "Суцільний обідок: повідомляють обидві сторони або незалежні джерела. Пунктирний обідок: заява. Тонкий обідок: одне джерело або преса однієї сторони.",
"Larger: more sources · fainter: older. Picture card: a major or fast-growing event with two or more sources.": "Більший маркер: більше джерел · блідіший: давніша подія. Картка з фото: велика подія або подія, що швидко розвивається, з двома чи більше джерелами.",
"Larger: more sources · fainter: older. Picture card: a major or fast-growing event with two or more sources.": "Більше: більше джерел · блідіше: давніше. Картка з фото: велика або швидко зростаюча подія з двох і більше джерел.",
"Side labels describe the source, not whether a report is true.": "Позначки сторін описують джерело, а не правдивість повідомлення.",
"Exit cockpit to fly to a news report": "Вийдіть із кабіни, щоб перелетіти до повідомлення",
"News items in the feed": "Кількість новин у стрічці",
"News items in the feed": "Новин у стрічці",
"Pick a shape, then click the map.": "Виберіть фігуру й клацніть на карті.",
"Enter to place the pin, Esc to cancel.": "Enter — поставити мітку, Esc — скасувати.",
"Click where the pin goes.": "Клацніть там, де має бути мітка.",
@@ -936,15 +936,15 @@
"{value} KM/S": "{value} КМ/С",
"{direction} {angle}°": "{direction} {angle}°",
"OFF-NADIR {angle}° · {offset}": "ВІДХИЛЕННЯ ВІД НАДИРА {angle}° · {offset}",
"SITES STANDBY": "ОБʼЄКТИ: ОЧІКУВАННЯ",
"ENABLE MAPPED INSTALLATIONS FOR SITE PINGS": "УВІМКНІТЬ «НАНЕСЕНІ ОБʼЄКТИ», ЩОБ БАЧИТИ КОНТАКТИ",
"SITES STANDBY": "ОБ'ЄКТИ: ОЧІКУВАННЯ",
"ENABLE MAPPED INSTALLATIONS FOR SITE PINGS": "УВІМКНІТЬ ПОЗНАЧЕНІ ОБ'ЄКТИ, ЩОБ БАЧИТИ МІСЦЯ",
"Release the camera from {name}": "Звільнити камеру від {name}",
"RETURN · {name}": "НАЗАД · {name}",
"PREVIOUS SATELLITE": "ПОПЕРЕДНІЙ СУПУТНИК",
"Return to {name}": "Повернутися до {name}",
"SATELLITE": "СУПУТНИК",
"Ride {name}": "Перейти на {name}",
"Unnamed site": "Безіменний обʼєкт",
"Unnamed site": "Безіменний об'єкт",
"OFF-NADIR {angle}°": "ВІДХИЛЕННЯ ВІД НАДИРА {angle}°",
"FOCUS · {name}": "ФОКУС · {name}",
"Keep the camera on {name}": "Утримувати камеру на {name}",
@@ -1253,9 +1253,9 @@
"Moving dots": "Рухомі точки",
"TomTom key unavailable": "Ключ TomTom недоступний",
"TomTom daily budget reached": "Вичерпано денний ліміт TomTom",
"Monthly traffic allowance used": "Місячний ліміт даних про дорожній рух вичерпано",
"Traffic rate limit reached": "Перевищено ліміт запитів до сервісу руху",
"Traffic service unavailable": "Сервіс руху недоступний",
"Monthly traffic allowance used": "Місячний ліміт даних про трафік вичерпано",
"Traffic rate limit reached": "Досягнуто ліміту запитів даних про трафік",
"Traffic service unavailable": "Сервіс даних про трафік недоступний",
"TomTom upstream unreachable": "Сервер TomTom не відповідає",
"TomTom flow unavailable": "Потік TomTom недоступний",
"SIMULATED — {reason}": "ІМІТАЦІЯ — {reason}",
@@ -1779,10 +1779,10 @@
"other": "{count} камери завантажено · увімкніть CCTV, щоб активувати"
},
"{count} more cameras outside this view · pan or zoom out": {
"one": "Ще {count} камера поза кадром · посуньте або віддаліть карту",
"few": "Ще {count} камери поза кадром · посуньте або віддаліть карту",
"many": "Ще {count} камер поза кадром · посуньте або віддаліть карту",
"other": "Ще {count} камери поза кадром · посуньте або віддаліть карту"
"one": "Ще {count} камера поза цим видом · посуньте або віддаліть",
"few": "Ще {count} камери поза цим видом · посуньте або віддаліть",
"many": "Ще {count} камер поза цим видом · посуньте або віддаліть",
"other": "Ще {count} камери поза цим видом · посуньте або віддаліть"
},
"Enable CCTV to load camera intersections": "Увімкніть CCTV, щоб завантажити камери на перехрестях",
"Enable CCTV to start camera-linked intelligence summaries.": "Увімкніть CCTV, щоб отримувати розвідувальні зведення за даними камер.",
@@ -1822,7 +1822,7 @@
"{scope} · INSTALLATIONS {coverage}": "{scope} · ОБʼЄКТИ: {coverage}",
"MILITARY FEED OFF · PARTIAL PICTURE": "ВІЙСЬКОВИЙ ПОТІК ВИМК. · КАРТИНА НЕПОВНА",
"FLIGHTS FEED OFF · PARTIAL PICTURE": "ПОТІК РЕЙСІВ ВИМК. · КАРТИНА НЕПОВНА",
"NEARBY AIRCRAFT INCOMPLETE · NOT AN ALL-CLEAR": "БОРТИ ПОБЛИЗУ: ДАНІ НЕПОВНІ · НЕ ОЗНАЧАЄ «УСЕ ЧИСТО»",
"NEARBY AIRCRAFT INCOMPLETE · NOT AN ALL-CLEAR": "БЛИЗЬКІ ПОВІТРЯНІ СУДНА: ДАНІ НЕПОВНІ · НЕ ОЗНАЧАЄ «УСЕ ЧИСТО»",
"CONTEXT STANDBY": "КОНТЕКСТ: ОЧІКУВАННЯ",
"ENABLE GLOBAL CONTEXT FOR PROXIMITY PINGS": "УВІМКНІТЬ ГЛОБАЛЬНИЙ КОНТЕКСТ, ЩОБ БАЧИТИ КОНТАКТИ ПОБЛИЗУ",
"CONTACT LOST · LAST KNOWN READOUT · NOT AN ALL-CLEAR": "КОНТАКТ ВТРАЧЕНО · ОСТАННІ ВІДОМІ ПОКАЗНИКИ · НЕ ОЗНАЧАЄ «УСЕ ЧИСТО»",
@@ -1859,7 +1859,7 @@
"ROUTE DATA UNAVAILABLE": "ДАНІ МАРШРУТУ НЕДОСТУПНІ",
"CONTACT NAVIGATION OFF": "НАВІГАЦІЮ КОНТАКТАМИ ВИМКНЕНО",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "МІСТОК ЗАЛИШАЄТЬСЯ НА ВИБРАНОМУ СУДНІ",
"{range} NM": "{range} МОР. МИЛ.",
"{range} NM": "{range} МОР. МИЛЬ",
"AIS DESTINATION · CREW REPORTED": "ПРИЗНАЧЕННЯ AIS · ВКАЗАНО ЕКІПАЖЕМ",
"DEST {bearing}": "ПРИЗН. {bearing}",
"Disable cockpit weather effects": "Вимкнути погодні ефекти кабіни",
@@ -2929,7 +2929,7 @@
"NO HEADING · AIS": "НЕМАЄ КУРСУ · AIS",
"VISOR LOCK · ACTIVE": "ФІКСАЦІЯ ВІЗОРА · АКТИВНА",
"GROUND SPEED · KTS": "ШЛЯХОВА ШВИДКІСТЬ · ВУЗ",
"SPEED OVER GROUND · KTS": "ШЛЯХОВА ШВИДКІСТЬ · ВУЗ",
"SPEED OVER GROUND · KTS": "ШВИДКІСТЬ ЩОДО ҐРУНТУ · ВУЗ",
"ALTITUDE · FT": "ВИСОТА · ФТ",
"FIRST PERSON": "ВІД ПЕРШОЇ ОСОБИ",
"BRIDGE VIEW": "ВИГЛЯД З МІСТКА",
@@ -2946,22 +2946,22 @@
"LOOK AHEAD": "ПОГЛЯД ПО НОСІ",
"Current vessel heading": "Поточний курс судна",
"GROUND SPEED": "ШЛЯХОВА ШВИДКІСТЬ",
"SPEED OVER GROUND": "ШЛЯХОВА ШВИДКІСТЬ",
"COURSE OVER GROUND": "ШЛЯХОВИЙ КУТ",
"SPEED OVER GROUND": "ШВИДКІСТЬ ЩОДО ҐРУНТУ",
"COURSE OVER GROUND": "КУРС ЩОДО ҐРУНТУ",
"KTS": "ВУЗ",
"ALTITUDE": "ВИСОТА",
"FT": "ФТ",
"Contact cockpit summary": "Зведення контакту в кабіні",
"CONTACT": "КОНТАКТ",
"CONTACTS · 250 KM": "КОНТАКТИ · 250 КМ",
"RINGS {spacing} NM": "КІЛЬЦЯ {spacing} МОР. МИЛ.",
"RINGS {spacing} NM": "КІЛЬЦЯ {spacing} МОР. МИЛЬ",
"{count} CONTACTS": {
"one": "{count} КОНТАКТ",
"few": "{count} КОНТАКТИ",
"many": "{count} КОНТАКТІВ",
"other": "{count} КОНТАКТУ"
},
"Radar range {range}. Select for the next range.": "Шкала дальності радара {range} — виберіть, щоб перейти до наступної шкали.",
"Radar range {range}. Select for the next range.": "Шкала дальності радара {range}. Виберіть, щоб перейти до наступної шкали.",
"Bridge radar": "Радар ходового містка",
"Contact navigation": "Навігація контактами",
"CONTEXT ONLY": "ЛИШЕ КОНТЕКСТ",
@@ -3064,7 +3064,7 @@
"AVAILABLE MISSIONS": "ДОСТУПНІ МІСІЇ",
"SELECT A MISSION TO INSPECT": "ВИБЕРІТЬ МІСІЮ ДЛЯ ПЕРЕГЛЯДУ",
"LOADING 30-DAY MISSION INDEX": "ЗАВАНТАЖЕННЯ ІНДЕКСУ МІСІЙ ЗА 30 ДНІВ",
"Space Missions unavailable: the mission index could not load. Try again later.": "Режим «Космічні місії» недоступний: не вдалося завантажити індекс місій. Спробуйте пізніше.",
"Space Missions unavailable: the mission index could not load. Try again later.": "Космічні місії недоступні: не вдалося завантажити список місій. Спробуйте пізніше.",
"30-DAY MISSION INDEX UNAVAILABLE": "ІНДЕКС МІСІЙ ЗА 30 ДНІВ НЕДОСТУПНИЙ",
"TAB PREVIEWS · ENTER / SPACE SELECTS": "TAB — ОГЛЯД · ENTER / SPACE — ВИБІР",
"Internet radio and local SDR": "Інтернет-радіо та локальний SDR",
@@ -3213,7 +3213,7 @@
"FLAT": "ПЛОСКІ",
"SINCE": "З ДАТИ",
"UPDATE SHOT": "ОНОВИТИ КАДР",
"IMPORT": "ІМПОРТ",
"IMPORT": "ІМПОРТУВАТИ",
"RUN LOG": "ЖУРНАЛ ВІДТВОРЕННЯ",
"POWER UP": "ЗАПУСК",
"GROUND STATION · PROVIDER SETTINGS": "НАЗЕМНА СТАНЦІЯ · НАЛАШТУВАННЯ ПОСТАЧАЛЬНИКІВ",
@@ -3282,13 +3282,13 @@
"+ CAPTURE SHOT": "+ ЗНЯТИ КАДР",
"Add the current view, style and layers as the next shot": "Додати поточний вигляд, стиль і шари як наступний кадр",
"Replace the selected shot with the current view": "Замінити вибраний кадр поточним виглядом",
"Click a shot to select it, double-click to rename.": "Клацніть кадр, щоб вибрати його, двічі — щоб перейменувати.",
"Click a shot to select it, double-click to rename.": "Натисніть кадр, щоб вибрати його, двічі — щоб перейменувати.",
"Play from this scene through the rest of the project": "Відтворити від цієї сцени до кінця проєкту",
"Stop playback (Esc)": "Зупинити відтворення (Esc)",
"Play the shot after the selected one, moving on to the next scene when needed": "Відтворити кадр після вибраного, за потреби перейшовши до наступної сцени",
"SAVE & SHARE": "ЗБЕРЕГТИ Й ПОДІЛИТИСЯ",
"Download every scene as a JSON file you can import later": "Завантажити всі сцени файлом JSON для подальшого імпорту",
"Load scenes from an exported JSON file": "Імпортувати сцени з експортованого файлу JSON",
"Load scenes from an exported JSON file": "Завантажити сцени з експортованого файлу JSON",
"Download the timing log of the last play": "Завантажити журнал таймінгів останнього відтворення",
"Edit anchors, data packs and shot details": "Змінити якорі, пакети даних і параметри кадрів",
"Download this scene as JSON or an asset bundle": "Завантажити цю сцену у форматі JSON або архівом ресурсів",
@@ -3303,7 +3303,7 @@
"Drawn shapes": "Намальовані фігури",
"Audio": "Аудіо",
"UTILITIES": "ІНСТРУМЕНТИ",
"Exit cockpit to fly to a drawing": "Вийдіть із кабіни, щоб перелетіти до фігури",
"Exit cockpit to fly to a drawing": "Вийдіть із кабіни, щоб перелетіти до малюнка",
"Directions, drawing and your own files": "Маршрути, малювання та ваші файли",
"Voice place search (needs an Overpass server)": "Голосовий пошук місць (потрібен сервер Overpass)",
"OSM Places": "Місця OSM",
@@ -3455,19 +3455,19 @@
"Last 24 h of news. Each report links to its publisher; places and categories are read from the text and data automatically and can be wrong. Side labels, where shown, describe the source, not whether a report is true.": "Новини за останні 24 год. Кожне повідомлення веде до свого видання; місця й категорії визначаються з тексту й даних автоматично і можуть бути хибними. Позначки сторін, де вони є, описують джерело, а не правдивість повідомлення.",
"Economy": "Економіка",
"News from {area}": "Новини: {area}",
"Within {km} km of the view centre": "У радіусі {km} км від центру карти",
"Within {km} km of the view centre": "У радіусі {km} км від центру мапи",
"{region}, {country}": "{region}, {country}",
"via {source}": "через {source}",
"Local in the News layer: the day’s news from the outlets of the country in view": "Розділ «Місцеві» в шарі «Новини»: новини дня від медіа країни в кадрі",
"Local in the News layer: the day’s news from the outlets of the country in view": "Місцеве в шарі новин: новини дня від медіа країни в кадрі",
"{provider} refused its key": "{provider} відхилив ключ",
"Check {envVars} in Provider Settings": "Перевірте {envVars} у «Налаштуваннях постачальників»",
"{provider} key required": "Потрібен ключ {provider}",
"{provider} limit reached for today": "{provider}: денний ліміт вичерпано",
"News resumes tomorrow, or with a paid plan": "Новини повернуться завтра або з платним тарифом",
"News key required": "Потрібен ключ новин",
"Precise conflict points in the News layer, beside GDELT’s articles": "Точні місця конфліктів у шарі «Новини», на додачу до статей GDELT",
"Precise conflict points in the News layer, beside GDELT’s articles": "Точні точки конфліктів у шарі новин, на додачу до статей GDELT",
"Worldwide": "Увесь світ",
"Shipwrecks": "Затонулі судна",
"Shipwrecks": "Корабельні аварії",
"Before 1900": "До 1900",
"After 1945": "Після 1945",
"Date unknown": "Дата невідома",
@@ -3480,13 +3480,13 @@
"Depth: {m} m": "Глибина: {m} м",
"Notes: {value}": "Примітки: {value}",
"Cargo or finds: {value}": "Вантаж або знахідки: {value}",
"Protected wreck": "Охоронюваний обʼєкт",
"Protected wreck": "Охоронюваний об'єкт",
"Source: {value}": "Джерело: {value}",
"Click the card to open EMODnet ↗": "Клацніть картку, щоб відкрити EMODnet ↗",
"Click the card to open the source ↗": "Клацніть картку, щоб відкрити джерело ↗",
"Click the card to open EMODnet ↗": "Натисніть картку, щоб відкрити EMODnet ↗",
"Click the card to open the source ↗": "Натисніть картку, щоб відкрити джерело ↗",
"Wikidata unavailable; showing EMODnet only": "Wikidata недоступна; показано лише EMODnet",
"EMODnet unavailable; showing Wikidata only": "EMODnet недоступний; показано лише Wikidata",
"Shipwrecks unavailable": "Дані про затонулі судна недоступні",
"Shipwrecks unavailable": "Дані про корабельні аварії недоступні",
"EMODnet (European waters) · Wikidata (worldwide)": "EMODnet (води Європи) · Wikidata (увесь світ)",
"{count} wrecks": {
"one": "{count} затонуле судно",
@@ -3495,25 +3495,25 @@
"other": "{count} затонулого судна"
},
"1900–1945": "1900–1945",
"Shipwreck": "Затонуле судно",
"Shipwreck": "Корабельна аварія",
"Fatalities: {count}": "Загиблих: {count}",
"Survivors: {count}": "Уцілілих: {count}",
"Click the card to open the article ↗": "Клацніть картку, щоб відкрити статтю ↗",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Історичні затонулі судна Європи — з EMODnet Human Activities, відомі затонулі судна з усього світу — з Wikidata. Колір показує, коли судно затонуло; маркер збільшується з кількістю загиблих. Клацніть маркер, щоб відкрити картку.",
"Survivors: {count}": "Тих, хто вижив: {count}",
"Click the card to open the article ↗": "Натисніть картку, щоб відкрити статтю ↗",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Історичні затонулі судна Європи — з EMODnet Human Activities, відомі корабельні аварії світу — з Wikidata. Колір показує, коли судно затонуло; маркер збільшується з кількістю загиблих. Натисніть маркер, щоб відкрити картку.",
"All years": "Усі роки",
"Period": "Період",
"From": "З",
"To": "По",
"First year of the shipwreck period": "Перший рік періоду загибелі суден",
"Last year of the shipwreck period": "Останній рік періоду загибелі суден",
"First year of the shipwreck period": "Перший рік періоду корабельних аварій",
"Last year of the shipwreck period": "Останній рік періоду корабельних аварій",
"SIGHTING": "СПОСТЕРЕЖЕННЯ",
"Show {name} sighting": "Показати спостереження: {name}",
"seen {age} ago": "помічено {age} тому",
"observed {age} ago": "спостерігалося {age} тому",
"Be Whale Wise": "Спостерігайте за китами дбайливо",
"Reported by {name} ({entity})": "Повідомляє: {name} ({entity})",
"Reported by {name}": "Повідомляє: {name}",
"Salish Sea": "море Селіш",
"Reported by {name} ({entity})": "Повідомив: {name} ({entity})",
"Reported by {name}": "Повідомив: {name}",
"Salish Sea": "Салішське море",
"Birds": "Птахи",
"Mammals": "Ссавці",
"Reptiles": "Плазуни",
@@ -3525,11 +3525,11 @@
"Fungi": "Гриби",
"Other animals": "Інші тварини",
"Other or unknown": "Інші або невідомі",
"{label} {detail} — click to retry": "{label}: {detail} — клацніть, щоб повторити",
"{label} {detail} — click to retry": "{label} {detail} — натисніть, щоб повторити",
"Wildlife sightings source unavailable": "Джерело спостережень живої природи недоступне",
"Show only community-confirmed (research-grade) sightings": "Показати лише підтверджені спільнотою спостереження дослідницького рівня",
"No recent sightings in view": "У кадрі немає недавніх спостережень",
"Acartia feed unavailable": "Потік Acartia недоступний",
"No recent sightings in view": "Немає недавніх спостережень в області огляду",
"Acartia feed unavailable": "Стрічка Acartia недоступна",
"Show all taxa": "Показати всі групи організмів",
"BIRDS": "ПТАХИ",
"Show only birds": "Показати лише птахів",
@@ -3544,31 +3544,31 @@
"All rights reserved": "Усі права захищено",
"Cetacean": "Китоподібне",
"Wildlife Sightings": "Спостереження живої природи",
"Exit cockpit to fly to a sighting": "Вийдіть із кабіни, щоб перелетіти до спостереження",
"Exit cockpit to fly to a sighting area": "Вийдіть із кабіни, щоб перелетіти до області спостереження",
"Exit cockpit to fly to a sighting": "Вийдіть із кабіни, щоб перейти до спостереження",
"Exit cockpit to fly to a sighting area": "Вийдіть із кабіни, щоб перейти до області спостереження",
"Wildlife": "Жива природа",
"Zoom in for local sightings": "Наблизьте карту, щоб побачити місцеві спостереження",
"Zoom in for local sightings": "Наблизьте для місцевих спостережень",
"Loading photo...": "Завантаження фото…",
"Photo unavailable": "Фото недоступне",
"Location obscured to a 0.2° cell": "Місцеположення приховано в комірці 0,2°",
"Photo of {name}": "Фото {name}",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{count} sightings in view": "Спостережень у кадрі: {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°. Не перепис.",
"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",
"Open {name} on Acartia": "Відкрити {name} в Acartia",
"iNaturalist": "iNaturalist",
"iNaturalist contributors · licences per record": "Учасники iNaturalist · ліцензія в кожного запису",
"iNaturalist contributors · Acartia {count} (Salish Sea) · licences per record": "Учасники iNaturalist · Acartia {count} (море Селіш) · ліцензія в кожного запису",
"{days}d": "{days} дн",
"All markers": "Усі мітки",
"Show markers of every age": "Показати мітки будь-якого віку",
"Maximum event age: {days}d; undated markers are kept": "Максимальний вік події: {days} дн; мітки без дати залишаються",
"{days}d": "{days}д",
"All markers": "Усі позначки",
"Show markers of every age": "Показати позначки будь-якого віку",
"Maximum event age: {days}d; undated markers are kept": "Максимальний вік події: {days} дн.; позначки без дати збережено",
"event age filter|All": "Усі",
"Event age": "Вік події",
"Event age: {days}d · undated kept": "Вік події: {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.": "Кадр на карті запитується окремо та може показувати інший момент.",
@@ -3581,24 +3581,24 @@
"Street level at {place}": "Вуличні знімки: {place}",
"Switching map": "Перемикання карти",
"Opening imagery": "Відкриття знімків",
"Street View is ready: press {open} to load it": "Перегляд вулиць готовий: натисніть «{open}», щоб завантажити",
"Street View is ready: press {open} to load it": "Перегляд вулиць готовий: натисніть {open}, щоб завантажити",
"{source} cannot open this link's street-level photo without a working key": "{source} не може відкрити вуличне фото за цим посиланням без робочого ключа",
"Street-level photos are hidden in the current view. Leave it first.": "Вуличні знімки приховані в поточному режимі. Спершу вийдіть із нього.",
"STATIONS": "СТАНЦІЇ",
"Crewed stations and the craft that visit them": "Пілотовані станції та кораблі, що до них прилітають",
"IMAGING": "ЗЙОМКА",
"Earth observation: optical, radar, weather and science": "Спостереження Землі: оптика, радар, погода й наука",
"TELECOM": "ЗВʼЯЗОК",
"Communications and navigation satellites, Starlink excepted": "Супутники звʼязку та навігації, крім Starlink",
"TELECOM": "ЗВ’ЯЗОК",
"Communications and navigation satellites, Starlink excepted": "Супутники зв’язку та навігації, крім Starlink",
"STARLINK": "STARLINK",
"The Starlink constellation (thousands of extra points)": "Угруповання Starlink (тисячі додаткових точок)",
"PROBES": "ЗОНДИ",
"Space science, technology demonstration and other payloads": "Космічна наука, демонстрація технологій та інші корисні навантаження",
"DEBRIS": "УЛАМКИ",
"Rocket bodies, fragments and dead satellites": "Ступені ракет, уламки й недіючі супутники",
"Rocket bodies, fragments and dead satellites": "Ступені ракет, уламки й непрацюючі супутники",
"Classes may lag: SATCAT is a cached copy": "Класи можуть відставати: SATCAT — кешована копія",
"Classes may lag: SATCAT unavailable": "Класи можуть відставати: SATCAT недоступний",
"CelesTrak SATCAT gives each object's type and status. Without a current copy the classes rest on the UCS database, names and CelesTrak groups, so a satellite that has died since may still show as working.": "CelesTrak SATCAT повідомляє тип і стан кожного обʼєкта. Без актуальної копії класи спираються на базу UCS, назви та групи CelesTrak, тож супутник, що відтоді вийшов з ладу, може й далі показуватися робочим.",
"CelesTrak SATCAT gives each object's type and status. Without a current copy the classes rest on the UCS database, names and CelesTrak groups, so a satellite that has died since may still show as working.": "CelesTrak SATCAT повідомляє тип і стан кожного об’єкта. Без актуальної копії класи спираються на базу UCS, назви та групи CelesTrak, тож супутник, що відтоді вийшов з ладу, може й далі показуватися робочим.",
"STARLINK ···": "STARLINK ···",
"STARLINK ✕": "STARLINK ✕",
"SENSOR SWATH {value} KM": "СМУГА СЕНСОРА {value} КМ",
@@ -3618,7 +3618,7 @@
"Zoom out (−)": "Віддалити (−)",
"ORBITAL VIEW · OFF-NADIR": "ОРБІТАЛЬНИЙ ВИГЛЯД · ВІДХИЛЕННЯ ВІД НАДИРА",
"World time": "Світовий час",
"Back to live": "Повернутися до поточного часу",
"Back to live": "Повернутися до прямого ефіру",
"Live": "Зараз",
"1 h/s": "1 год/с",
"1 d/s": "1 дн/с",
@@ -3636,22 +3636,20 @@
"Show {category}": "Показати: {category}",
"Live GPS": "GPS у реальному часі",
"Estimated from station reports": "Оцінка за відмітками станцій",
"To {headsign}": "Куди: {headsign}",
"Next {stop} {time} {relative}": "Наступна: {stop} {time} {relative}",
"Prev {stop} {time} {relative}": "Попередня: {stop} {time} {relative}",
"To {headsign}": "До {headsign}",
"Next {stop} {time} {relative}": "Наступна {stop} {time} {relative}",
"Prev {stop} {time} {relative}": "Попередня {stop} {time} {relative}",
"(+{minutes} min late)": "(+{minutes} хв запізнення)",
"Exit cockpit to fly to a train": "Вийдіть із кабіни, щоб перелетіти до поїзда",
"Photo © {photographer}": "Фото © {photographer}",
"Photo: {photographer} · {licence}": "Фото: {photographer} · {licence}",
"Photo · {licence}": "Фото · {licence}",
"The shared flight isn't reporting now — showing where it was last seen": "Рейс за посиланням зараз не передає даних — показано останнє відоме місце",
"The shared flight isn't reporting now — showing where it was at {time}": "Рейс за посиланням зараз не передає даних — показано місце станом на {time}",
"The shared military flight isn't reporting now — showing where it was last seen": "Військовий рейс за посиланням зараз не передає даних — показано останнє відоме місце",
"The shared military flight isn't reporting now — showing where it was at {time}": "Військовий рейс за посиланням зараз не передає даних — показано місце станом на {time}",
"The shared vessel isn't reporting now — showing where it was last seen": "Судно за посиланням зараз не передає даних — показано останнє відоме місце",
"The shared vessel isn't reporting now — showing where it was at {time}": "Судно за посиланням зараз не передає даних — показано місце станом на {time}",
"The shared satellite isn't reporting now — showing where it was last seen": "Супутник за посиланням зараз не передає даних — показано останнє відоме місце",
"The shared satellite isn't reporting now — showing where it was at {time}": "Супутник за посиланням зараз не передає даних — показано місце станом на {time}",
"The shared flight isn't reporting now — showing where it was last seen": "Спільний рейс зараз не передає даних — показано останнє відоме місце",
"The shared flight isn't reporting now — showing where it was at {time}": "Спільний рейс зараз не передає даних — показано місце о {time}",
"The shared military flight isn't reporting now — showing where it was last seen": "Спільний військовий рейс зараз не передає даних — показано останнє відоме місце",
"The shared military flight isn't reporting now — showing where it was at {time}": "Спільний військовий рейс зараз не передає даних — показано місце о {time}",
"The shared vessel isn't reporting now — showing where it was last seen": "Спільне судно зараз не передає даних — показано останнє відоме місце",
"The shared vessel isn't reporting now — showing where it was at {time}": "Спільне судно зараз не передає даних — показано місце о {time}",
"The shared satellite isn't reporting now — showing where it was last seen": "Спільний супутник зараз не передає даних — показано останнє відоме місце",
"The shared satellite isn't reporting now — showing where it was at {time}": "Спільний супутник зараз не передає даних — показано місце о {time}",
"ICAO: {id}": "ICAO: {id}",
"NORAD: {id}": "NORAD: {id}",
"MMSI: {id}": "MMSI: {id}",
@@ -3659,9 +3657,9 @@
"Latitude must be within ±90° and longitude within ±180°": "Широта має бути в межах ±90°, а довгота — у межах ±180°",
"Street Level is unavailable": "Вуличні знімки недоступні",
"No street-level imagery source is configured": "Джерело вуличних знімків не налаштовано",
"Street Level could not be enabled": "Не вдалося увімкнути «Вуличні знімки»",
"Street Level changed while it looked for imagery": "Стан шару «Вуличні знімки» змінився під час пошуку",
"Aim the view at a street": "Спрямуйте камеру на вулицю",
"Street Level could not be enabled": "Не вдалося увімкнути вуличні знімки",
"Street Level changed while it looked for imagery": "Вуличні знімки змінилися під час пошуку",
"Aim the view at a street": "Наведіть вигляд на вулицю",
"{source} is not a street-level imagery source here": "{source} тут не є джерелом вуличних знімків",
"{source} needs its key": "{source}: потрібен ключ",
"{source} needs the {map} map, which is not available": "{source}: потрібна карта {map}, але вона недоступна",
@@ -3669,14 +3667,14 @@
"{source} is not available on this map": "{source}: недоступно на цій карті",
"{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 {place}": "Немає знімків {source} поблизу {place}",
"{reason} (filters on: {filters})": "{reason} (увімкнено фільтри: {filters})",
"tiles unavailable": "тайли недоступні",
"zoom in to load this layer": "наблизьте карту, щоб завантажити цей шар",
"zoom in to load this layer": "наблизьте, щоб завантажити цей шар",
"Zoom to layer": "Наблизити до шару",
"Zoom to layer · fly to where this layer has data": "Наблизити до шару · перелетіти туди, де цей шар має дані",
"Remove from Data Layers": "Видалити з «Шарів даних»",
"Remove from Data Layers · add it again from the library": "Видалити з «Шарів даних» · можна знову додати з бібліотеки",
"Remove from Data Layers": "Вилучити з «Шарів даних»",
"Remove from Data Layers · add it again from the library": "Вилучити з «Шарів даних» · можна знову додати з бібліотеки",
"Coverage outline": "Контур покриття",
"DRAPED": "НАКЛАДЕНО",
"Below 250 m": "Нижче 250 м",
@@ -3693,7 +3691,7 @@
"DATASET": "НАБІР ДАНИХ",
"TILES": "ТАЙЛИ",
"PACK": "ПАКЕТ",
"INCUBATING": "НА ВИПРОБУВАННІ",
"INCUBATING": "НА ІНКУБАЦІЇ",
"COMMUNITY": "СПІЛЬНОТА",
"VERIFIED": "ПЕРЕВІРЕНО",
"ARCHIVED": "В АРХІВІ",
@@ -3726,58 +3724,58 @@
"other": "{count} на карті"
},
"none yet": "поки жодного",
"Map features": "Обʼєкти карти",
"Map features": "Об’єкти карти",
"from {count} datasets": {
"one": "з {count} набору даних",
"few": "з {count} наборів даних",
"many": "з {count} наборів даних",
"other": "з {count} набору даних"
},
"switch on a dataset to load features": "увімкніть набір даних, щоб завантажити обʼєкти",
"switch on a dataset to load features": "увімкніть набір даних, щоб завантажити об’єкти",
"Switched on by this instance · part of the CCTV layer": "Увімкнено цим екземпляром · частина шару CCTV",
"Operator opt-in: set {env}=1 on the server": "Увімкнення оператором: задайте {env}=1 на сервері",
"Operator opt-in: add {id} to GEV_LIBRARY_ENABLE on the server": "Увімкнення оператором: додайте {id} до GEV_LIBRARY_ENABLE на сервері",
"Turned off by this instance's operator": "Вимкнено оператором цього екземпляра",
"Needs a provider key on the server": "Потрібен ключ постачальника на сервері",
"Needs this app's server · not available in a static build": "Потрібен сервер цього застосунку · недоступно в статичній збірці",
"Checking what this instance serves…": "Перевірка того, що надає цей екземпляр…",
"Regional": "Регіонально",
"Checking what this instance serves…": "Перевірка, що надає цей екземпляр…",
"Regional": "Регіональні",
"Orthophoto": "Ортофото",
"Satellite mosaic": "Супутникова мозаїка",
"Oblique": "Перспективна зйомка",
"Historic imagery": "Історичні знімки",
"True orthophoto": "Істинне ортофото",
"True orthophoto": "Справжнє ортофото",
"Type": "Тип",
"Resolution": "Роздільна здатність",
"Resolution": "Роздільність",
"Flown": "Зйомка",
"Updates": "Оновлення",
"Ortho quality": "Якість орто",
"DATA LIBRARY": "БІБЛІОТЕКА ДАНИХ",
"Close data library": "Закрити бібліотеку даних",
"Data Library at a glance": "Огляд бібліотеки даних",
"Live sources beyond the core layers. Each one is fetched from its provider while it is on; nothing is stored in this app. Add one and it appears in Data Layers under Library.": "Джерела даних у реальному часі поза основними шарами. Кожне завантажується від свого постачальника, поки увімкнене; цей застосунок нічого не зберігає. Додайте джерело, і воно зʼявиться в «Шарах даних» у розділі «Бібліотека».",
"Data Library at a glance": "Бібліотека даних коротко",
"Live sources beyond the core layers. Each one is fetched from its provider while it is on; nothing is stored in this app. Add one and it appears in Data Layers under Library.": "Живі джерела поза основними шарами. Кожне завантажується від свого постачальника, поки увімкнене; цей застосунок нічого не зберігає. Додайте джерело, і воно з’явиться в «Шарах даних» у розділі «Бібліотека».",
"Name, provider or country": "Назва, постачальник або країна",
"Theme": "Тема",
"All themes": "Усі теми",
"Only what this instance serves": "Лише те, що надає цей екземпляр",
"OPT-IN": "ВМИКАЄ ОПЕРАТОР",
"OPT-IN": "ЗА ЗГОДОЮ",
"KEY": "КЛЮЧ",
"KEYLESS": "БЕЗ КЛЮЧА",
"ZOOM": "НАБЛИЗИТИ",
"Zoom to {name}": "Наблизити до {name}",
"WORKING…": "ОБРОБКА…",
"ADD": "ДОДАТИ",
"Remove {name} from Data Layers": "Видалити {name} з «Шарів даних»",
"Remove {name} from Data Layers": "Вилучити {name} з «Шарів даних»",
"Add {name} to Data Layers": "Додати {name} до «Шарів даних»",
"CONFIRM · REMOVE ALL {count}": {
"one": "ПІДТВЕРДИТИ · ВИДАЛИТИ {count}",
"few": "ПІДТВЕРДИТИ · ВИДАЛИТИ ВСІ {count}",
"many": "ПІДТВЕРДИТИ · ВИДАЛИТИ ВСІ {count}",
"other": "ПІДТВЕРДИТИ · ВИДАЛИТИ ВСІ {count}"
"one": "ПІДТВЕРДИТИ · ВИЛУЧИТИ {count}",
"few": "ПІДТВЕРДИТИ · ВИЛУЧИТИ ВСІ {count}",
"many": "ПІДТВЕРДИТИ · ВИЛУЧИТИ ВСІ {count}",
"other": "ПІДТВЕРДИТИ · ВИЛУЧИТИ ВСІ {count}"
},
"REMOVE ALL": "ВИДАЛИТИ ВСЕ",
"Confirm: remove every added entry from Data Layers": "Підтвердити: видалити всі додані записи з «Шарів даних»",
"Remove every added entry from Data Layers": "Видалити всі додані записи з «Шарів даних»",
"REMOVE ALL": "ВИЛУЧИТИ ВСЕ",
"Confirm: remove every added entry from Data Layers": "Підтвердити: вилучити всі додані записи з «Шарів даних»",
"Remove every added entry from Data Layers": "Вилучити всі додані записи з «Шарів даних»",
"Showing {shown} of {total}": "Показано {shown} з {total}",
"BROWSE DATA LIBRARY": "ВІДКРИТИ БІБЛІОТЕКУ ДАНИХ"
}
+87 -89
View File
@@ -10,12 +10,12 @@
"Disputed: {name}": "争议地区:{name}",
"Video": "视频",
"Photos": "照片",
"Claimed by {who}": "{who} 声称",
"Claimed by {who}": "{who}声称",
"Claimed": "声称",
"Not yet confirmed by others": "尚未获他方证实",
"Reported by {side} sources": "报道来源:{side}",
"Reported by {side} sources": "{side}方来源报道",
"Reported by {sides}": "报道来源:{sides}",
"{side} sources": "{side}",
"{side} sources": "{side}方来源",
"Official": "官方",
"Reported by both sides": "双方均有报道",
"Independently reported": "有独立来源报道",
@@ -23,7 +23,7 @@
"International": "国际",
"Independent": "独立",
"Unattributed": "未注明来源",
"Outside party": "第三方",
"Outside party": "外部方",
"Ukraine–Russia": "乌克兰–俄罗斯",
"Israel–Gaza–Lebanon": "以色列–加沙–黎巴嫩",
"Iran–Hormuz": "伊朗–霍尔木兹",
@@ -36,17 +36,17 @@
"Air defence": "防空",
"Ground fighting": "地面战斗",
"Casualties": "伤亡",
"Humanitarian": "人道主义",
"Humanitarian": "人道",
"Disaster": "灾害",
"Protest": "抗议",
"Political": "政治",
"No physical event": "无实际事件",
"Minor incident": "轻微事件",
"Significant": "较大",
"Major": "重大",
"Significant": "重大",
"Major": "严重",
"age|now": "刚刚",
"{minutes} min": "{minutes} 分钟前",
"{hours} h": "{hours} 小时前",
"{minutes} min": "{minutes} 分钟",
"{hours} h": "{hours} 小时",
"{count} sources": {
"other": "{count} 个来源"
},
@@ -78,10 +78,10 @@
"Load older news": "加载更早的新闻",
"How to read the map": "如何解读地图",
"Colour: severity · icon: type of event": "颜色:严重程度 · 图标:事件类型",
"Solid rim: reported by both sides or independently. Dashed rim: a claim. Thin rim: one source, or one side’s own press.": "实线边框:双方均有报道,或有独立来源报道。虚线边框:一方声称。细边框:单一来源,或一方自身的媒体。",
"Larger: more sources · fainter: older. Picture card: a major or fast-growing event with two or more sources.": "越大:来源越多 · 越淡:越早。图片卡片:有两个或以上来源的重大或快速发展的事件。",
"Solid rim: reported by both sides or independently. Dashed rim: a claim. Thin rim: one source, or one side’s own press.": "实线边框:双方或独立来源均有报道。虚线边框:一方声称。细边框:单一来源,或一方自身的媒体。",
"Larger: more sources · fainter: older. Picture card: a major or fast-growing event with two or more sources.": "越大:来源越多 · 越淡:越早。图片卡片:有两个或以上来源的严重或快速发展的事件。",
"Side labels describe the source, not whether a report is true.": "阵营标签描述的是来源,而非报道是否属实。",
"Exit cockpit to fly to a news report": "请先退出座舱,再飞往新闻报道地点",
"Exit cockpit to fly to a news report": "退出驾驶舱以飞往新闻报道",
"News items in the feed": "列表中的新闻数",
"Pick a shape, then click the map.": "先选择形状,再在地图上点击。",
"Enter to place the pin, Esc to cancel.": "按 Enter 放置图钉,按 Esc 取消。",
@@ -598,7 +598,7 @@
"FOV {value}° (DATASHEET)": "FOV {value}°(规格书)",
"FOV {value}°": "FOV {value}°",
"SPEC FOV {span}° (CAPABILITY)": "规格 FOV {span}°(可调范围)",
"COVERAGE {value}km²": "覆盖 {value} km²",
"COVERAGE {value}km²": "覆盖 {value}km²",
"OVERLAP {count} cams": "重叠 {count} 个摄像头",
"ISOLATED VIEW": "独立视角",
"PROJ MONITOR": "投影:监视器",
@@ -616,8 +616,8 @@
"Hurricane": "飓风",
"Tropical storm": "热带风暴",
"Tropical depression": "热带低压",
"Subtropical storm": "副热带风暴",
"Subtropical depression": "副热带低压",
"Subtropical storm": "亚热带风暴",
"Subtropical depression": "亚热带低压",
"Track and cone match this advisory": "路径和预报不确定范围与本期公告一致",
"Track/cone awaiting advisory {number}": "路径/预报不确定范围等待第 {number} 号公告",
"Track/cone unavailable": "路径/预报不确定范围不可用",
@@ -808,7 +808,7 @@
"Low-accuracy score 2–10%": "低精度评分 2–10%",
"Low-accuracy score > 10%": "低精度评分 > 10%",
"GNSS integrity source unavailable": "GNSS 完好性数据源不可用",
"Regional sample · 250 nm radius · cells from visited views over {minutes} min": "区域采样 · 半径 250 海里 · {minutes} 分钟内浏览过的视野中的网格",
"Regional sample · 250 nm radius · cells from visited views over {minutes} min": "区域采样 · 半径 250 海里 · {minutes} 分钟内已访问视图中的网格",
"Retrying automatically at {time} UTC": "将于 {time} UTC 自动重试",
"Retrying automatically after provider cooldown": "服务商冷却结束后自动重试",
"Updates automatically after pan or zoom and once a minute. The public feed covers a 250-nautical-mile circle around the view centre; zooming out does not make it a worldwide snapshot. Blank cells are outside sampled coverage or have insufficient observations.": "平移或缩放后以及每分钟自动更新。公共数据源覆盖以视野中心为圆心、半径 250 海里的范围;缩小地图并不会使其成为全球快照。空白网格位于采样覆盖范围之外,或观测数据不足。",
@@ -866,18 +866,18 @@
"{value} KM/S": "{value} km/s",
"{direction} {angle}°": "{direction} {angle}°",
"OFF-NADIR {angle}° · {offset}": "偏离天底 {angle}° · {offset}",
"SITES STANDBY": "设施待机",
"ENABLE MAPPED INSTALLATIONS FOR SITE PINGS": "启用地图标注设施以接收设施提醒",
"Release the camera from {name}": "让镜头离开 {name}",
"SITES STANDBY": "地点待机",
"ENABLE MAPPED INSTALLATIONS FOR SITE PINGS": "启用已测绘设施以接收地点提醒",
"Release the camera from {name}": "让镜头离开{name}",
"RETURN · {name}": "返回 · {name}",
"PREVIOUS SATELLITE": "上一颗卫星",
"Return to {name}": "返回 {name}",
"Return to {name}": "返回{name}",
"SATELLITE": "卫星",
"Ride {name}": "乘坐 {name}",
"Unnamed site": "未命名设施",
"Ride {name}": "乘坐{name}",
"Unnamed site": "未命名地点",
"OFF-NADIR {angle}°": "偏离天底 {angle}°",
"FOCUS · {name}": "聚焦 · {name}",
"Keep the camera on {name}": "让镜头持续对准 {name}",
"Keep the camera on {name}": "让镜头持续对准{name}",
"ZOOM {zoom}×": "变焦 {zoom}×",
"SWATH {value} KM": "幅宽 {value} km",
"SWATH ≈ {value} KM": "幅宽 ≈ {value} km",
@@ -885,10 +885,10 @@
"ORBITAL VIEW · NADIR": "轨道视图 · 天底",
"ORBITAL VELOCITY": "轨道速度",
"SUB-SATELLITE POINT": "星下点",
"NORTH UP": "正北朝上",
"CURRENTLY OVER": "当前飞越",
"{division}, {country}": "{country} {division}",
"{region} · near {place}": "{region} · {place} 附近",
"NORTH UP": "北向上",
"CURRENTLY OVER": "当前上空",
"{division}, {country}": "{division},{country}",
"{region} · near {place}": "{region} · {place}附近",
"NORAD {id}": "NORAD {id}",
"EPOCH {time}": "历元 {time}",
"TLE": "TLE",
@@ -1173,7 +1173,7 @@
"Moving dots": "移动圆点",
"TomTom key unavailable": "TomTom 密钥不可用",
"TomTom daily budget reached": "已达 TomTom 每日配额",
"Monthly traffic allowance used": "本月交通数据配额已用完",
"Monthly traffic allowance used": "本月交通流量额度已用完",
"Traffic rate limit reached": "已达到交通数据请求频率上限",
"Traffic service unavailable": "交通服务不可用",
"TomTom upstream unreachable": "TomTom 上游服务无法连接",
@@ -1606,7 +1606,7 @@
"Link copied!": "链接已复制!",
"Link shared": "链接已分享",
"This share link can't be opened": "无法打开此分享链接",
"Shared vessel is unavailable": "分享的船舶不可用",
"Shared vessel is unavailable": "分享的船只不可用",
"Copy failed": "复制失败",
"Return map to straight-down view": "将地图恢复为垂直俯视",
"Tilt map to oblique view": "将地图倾斜至斜视角",
@@ -1676,7 +1676,7 @@
"other": "已加载 {count} 个摄像头 · 启用 CCTV 以激活"
},
"{count} more cameras outside this view · pan or zoom out": {
"other": "视野外还有 {count} 个摄像头 · 请平移或缩小地图"
"other": "此视图外还有 {count} 个摄像头 · 平移或缩小"
},
"Enable CCTV to load camera intersections": "启用 CCTV 以加载路口摄像头",
"Enable CCTV to start camera-linked intelligence summaries.": "启用 CCTV 以开始生成与摄像头关联的情报摘要。",
@@ -1716,7 +1716,7 @@
"{scope} · INSTALLATIONS {coverage}": "{scope} · 设施 {coverage}",
"MILITARY FEED OFF · PARTIAL PICTURE": "军用数据源已关闭 · 态势不完整",
"FLIGHTS FEED OFF · PARTIAL PICTURE": "航班数据源已关闭 · 态势不完整",
"NEARBY AIRCRAFT INCOMPLETE · NOT AN ALL-CLEAR": "附近飞机信息不完整 · 不代表安全无虞",
"NEARBY AIRCRAFT INCOMPLETE · NOT AN ALL-CLEAR": "附近航空器信息不完整 · 不代表安全无虞",
"CONTEXT STANDBY": "态势待机",
"ENABLE GLOBAL CONTEXT FOR PROXIMITY PINGS": "启用全局态势以接收邻近目标提醒",
"CONTACT LOST · LAST KNOWN READOUT · NOT AN ALL-CLEAR": "目标丢失 · 最后已知读数 · 不代表安全无虞",
@@ -1750,7 +1750,7 @@
"ROUTE DATA UNAVAILABLE": "航线数据不可用",
"CONTACT NAVIGATION OFF": "目标导航已关闭",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "驾驶台保持在所选船舶上",
"{range} NM": "{range} 海里",
"{range} NM": "{range}海里",
"AIS DESTINATION · CREW REPORTED": "AIS 目的地 · 船员申报",
"DEST {bearing}": "目的地 {bearing}",
"Disable cockpit weather effects": "停用座舱天气效果",
@@ -1870,7 +1870,7 @@
"GNSS Integrity": "GNSS 完好性",
"Rain radar": "降雨雷达",
"Lightning density": "闪电密度",
"Cyclone advisories": "气旋公告",
"Cyclone advisories": "气旋预警",
"ALPR Cameras": "ALPR 摄像头",
"Live Vessels": "实时船舶",
"Bike Share": "共享单车",
@@ -2073,8 +2073,8 @@
"coverage|partial": "部分",
"unknown": "未知",
"no imagery": "无影像",
"{percent}% cloud": "云量 {percent}%",
"{min}–{max}% cloud": "云量 {min}–{max}%",
"{percent}% cloud": "{percent}% 云量",
"{min}–{max}% cloud": "{min}–{max}% 云量",
"NO BOX": "无框选",
"SEARCHING": "搜索中",
"{count} DAYS": {
@@ -2629,7 +2629,7 @@
"Contact cockpit summary": "目标座舱摘要",
"CONTACT": "目标",
"CONTACTS · 250 KM": "目标 · 250 km",
"RINGS {spacing} NM": "距标圈 {spacing} 海里",
"RINGS {spacing} NM": "距标圈 {spacing}海里",
"{count} CONTACTS": {
"other": "{count} 个目标"
},
@@ -2718,7 +2718,7 @@
"WEATHER": "天气",
"Active weather products": "已启用的天气产品",
"RECENT IMAGERY": "近期影像",
"Imagery days in the box": "框选区域内的影像天数",
"Imagery days in the box": "视野内的影像天数",
"CONTEXT": "态势",
"Context mode": "态势模式",
"CONTACTS": "目标",
@@ -3078,7 +3078,7 @@
"Left panels": "左侧面板",
"Right panels": "右侧面板",
"UTILITIES": "工具",
"Exit cockpit to fly to a drawing": "请先退出座舱,再飞往已绘制的形状",
"Exit cockpit to fly to a drawing": "请先退出座舱,再飞往绘制位置",
"Directions, drawing and your own files": "路线导航、绘制和你自己的文件",
"Voice place search (needs an Overpass server)": "语音地点搜索(需要 Overpass 服务器)",
"OSM Places": "OSM 地点",
@@ -3214,11 +3214,11 @@
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "服务商账户余额不足。请检查账户的计费信息,或在设置中选择其他 AI 服务商。",
"Last 24 h of news. Each report links to its publisher; places and categories are read from the text and data automatically and can be wrong. Side labels, where shown, describe the source, not whether a report is true.": "最近 24 小时的新闻。每条报道都链接到其发布者;地点和类别由文本和数据自动读取,可能有误。阵营标签(如有显示)描述的是来源,而非报道是否属实。",
"Economy": "经济",
"News from {area}": "{area} 新闻",
"Within {km} km of the view centre": "视野中心 {km} km 范围内",
"{region}, {country}": "{country} {region}",
"News from {area}": "{area}新闻",
"Within {km} km of the view centre": "视图中心 {km} 公里范围内",
"{region}, {country}": "{country}{region}",
"via {source}": "来自 {source}",
"Local in the News layer: the day’s news from the outlets of the country in view": "新闻图层的“本地”分区:当前视野所在国家的媒体当天的新闻",
"Local in the News layer: the day’s news from the outlets of the country in view": "新闻图层的本地:视野中国家的媒体当天的新闻",
"{provider} refused its key": "{provider} 拒绝了密钥",
"Check {envVars} in Provider Settings": "请在服务商设置中检查 {envVars}",
"{provider} key required": "需要 {provider} 密钥",
@@ -3228,16 +3228,16 @@
"Precise conflict points in the News layer, beside GDELT’s articles": "新闻图层中精确的冲突地点,与 GDELT 的文章并列",
"Worldwide": "全球",
"Shipwrecks": "沉船",
"Before 1900": "1900 年以前",
"After 1945": "1945 年以后",
"Before 1900": "1900年以前",
"After 1945": "1945年以后",
"Date unknown": "日期不详",
"Position: centre of the protected area": "位置:保护区中心",
"Position approximate (within about {km} km)": "大致位置(误差约 {km} km)",
"Position surveyed (within about {m} m)": "实测位置(误差约 {m} m)",
"Vessel: {value}": "船型:{value}",
"Position approximate (within about {km} km)": "大致位置(误差约 {km} 公里)",
"Position surveyed (within about {m} m)": "实测位置(误差约 {m} 米)",
"Vessel: {value}": "船只:{value}",
"What happened: {value}": "经过:{value}",
"Depth: {m} m ({quality})": "深度:{m} m({quality})",
"Depth: {m} m": "深度:{m} m",
"Depth: {m} m ({quality})": "深度:{m} 米({quality})",
"Depth: {m} m": "深度:{m} 米",
"Notes: {value}": "备注:{value}",
"Cargo or finds: {value}": "货物或出水文物:{value}",
"Protected wreck": "受保护沉船",
@@ -3253,8 +3253,8 @@
},
"1900–1945": "1900–1945",
"Shipwreck": "沉船",
"Fatalities: {count}": "遇难人数:{count}",
"Survivors: {count}": "幸存人数:{count}",
"Fatalities: {count}": "遇难:{count}",
"Survivors: {count}": "幸存者:{count}",
"Click the card to open the article ↗": "点击卡片打开文章 ↗",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "欧洲历史沉船来自 EMODnet Human Activities;全球知名沉船来自 Wikidata。颜色表示船只沉没的时期,标记大小随遇难人数增加。点击标记查看卡片。",
"All years": "所有年份",
@@ -3264,12 +3264,12 @@
"First year of the shipwreck period": "沉船时段的起始年份",
"Last year of the shipwreck period": "沉船时段的结束年份",
"SIGHTING": "目击",
"Show {name} sighting": "显示 {name} 的目击记录",
"Show {name} sighting": "显示{name}目击",
"seen {age} ago": "{age}前目击",
"observed {age} ago": "{age}前观察到",
"observed {age} ago": "{age}前观察",
"Be Whale Wise": "负责任地观鲸",
"Reported by {name} ({entity})": "报告方:{name}({entity})",
"Reported by {name}": "报告方:{name}",
"Reported by {name} ({entity})": "报告方 {name}({entity})",
"Reported by {name}": "报告方 {name}",
"Salish Sea": "萨利希海",
"Birds": "鸟类",
"Mammals": "哺乳类",
@@ -3282,18 +3282,18 @@
"Fungi": "真菌",
"Other animals": "其他动物",
"Other or unknown": "其他或未知",
"{label} {detail} — click to retry": "{label} {detail}——点击重试",
"{label} {detail} — click to retry": "{label} {detail} — 点击重试",
"Wildlife sightings source unavailable": "野生生物目击来源不可用",
"Show only community-confirmed (research-grade) sightings": "仅显示社区确认(研究级)的目击",
"No recent sightings in view": "视野内无近期目击",
"Acartia feed unavailable": "Acartia 数据源不可用",
"Acartia feed unavailable": "Acartia数据源不可用",
"Show all taxa": "显示所有分类群",
"BIRDS": "鸟类",
"Show only birds": "仅显示鸟类",
"MAMMALS": "哺乳类",
"Show only mammals": "仅显示哺乳类",
"WHALES": "鲸类",
"Show only whales and dolphins (7 days)": "仅显示鲸和海豚(7 天)",
"Show only whales and dolphins (7 days)": "仅显示鲸和海豚(7天)",
"INSECTS": "昆虫",
"Show only insects": "仅显示昆虫",
"PLANTS": "植物",
@@ -3301,31 +3301,31 @@
"All rights reserved": "保留所有权利",
"Cetacean": "鲸类",
"Wildlife Sightings": "野生生物目击",
"Exit cockpit to fly to a sighting": "请先退出座舱,再飞往目击地点",
"Exit cockpit to fly to a sighting area": "请先退出座舱,再飞往目击区域",
"Exit cockpit to fly to a sighting": "退出驾驶舱以飞往目击地点",
"Exit cockpit to fly to a sighting area": "退出驾驶舱以飞往目击区域",
"Wildlife": "野生生物",
"Zoom in for local sightings": "放大查看当地目击",
"Loading photo...": "正在加载照片…",
"Photo unavailable": "照片不可用",
"Location obscured to a 0.2° cell": "位置已模糊至 0.2° 网格",
"Photo of {name}": "{name} 的照片",
"Location obscured to a 0.2° cell": "位置已模糊至0.2°网格",
"Photo of {name}": "{name}的照片",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{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°。并非种群普查。",
"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}",
"Open {name} on Acartia": "在 Acartia 上打开 {name}",
"iNaturalist": "iNaturalist",
"iNaturalist contributors · licences per record": "iNaturalist 贡献者 · 各记录单独授权",
"iNaturalist contributors · Acartia {count} (Salish Sea) · licences per record": "iNaturalist 贡献者 · Acartia {count}(萨利希海)· 各记录单独授权",
"{days}d": "{days} 天",
"iNaturalist contributors · Acartia {count} (Salish Sea) · licences per record": "iNaturalist 贡献者 · Acartia {count}(萨利希海) · 各记录单独授权",
"{days}d": "{days}天",
"All markers": "所有标记",
"Show markers of every age": "显示所有时间的标记",
"Maximum event age: {days}d; undated markers are kept": "仅显示 {days} 天内的事件;保留无日期标记",
"Maximum event age: {days}d; undated markers are kept": "事件最长保留时间:{days}天;保留无日期标记",
"event age filter|All": "全部",
"Event age": "事件时间",
"Event age: {days}d · undated kept": "事件时间:{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.": "地图静止画面通过单独的请求获取,可能显示不同的时刻。",
@@ -3333,12 +3333,12 @@
"Press OPEN to load the photo": "点击“打开”加载照片",
"Captured {month}": "拍摄于 {month}",
"this spot": "此处",
"the view centre": "视野中心",
"the view centre": "视图中心",
"{source} at {place}": "{place} 的 {source}",
"Street level at {place}": "{place} 的街景",
"Switching map": "正在切换地图",
"Opening imagery": "正在打开影像",
"Street View is ready: press {open} to load it": "街景已就绪:点击“{open}”加载",
"Street View is ready: press {open} to load it": "街景已就绪:按 {open} 加载",
"{source} cannot open this link's street-level photo without a working key": "没有有效密钥,{source} 无法打开此链接的街景照片",
"Street-level photos are hidden in the current view. Leave it first.": "当前视图隐藏了街景照片。请先退出该视图。",
"STATIONS": "空间站",
@@ -3352,7 +3352,7 @@
"PROBES": "探测器",
"Space science, technology demonstration and other payloads": "空间科学、技术验证及其他载荷",
"DEBRIS": "碎片",
"Rocket bodies, fragments and dead satellites": "火箭箭体、碎片和失效卫星",
"Rocket bodies, fragments and dead satellites": "火箭残骸、碎片和失效卫星",
"Classes may lag: SATCAT is a cached copy": "分类可能滞后:SATCAT 为缓存副本",
"Classes may lag: SATCAT unavailable": "分类可能滞后:SATCAT 不可用",
"CelesTrak SATCAT gives each object's type and status. Without a current copy the classes rest on the UCS database, names and CelesTrak groups, so a satellite that has died since may still show as working.": "CelesTrak SATCAT 给出每个物体的类型和运行状态。没有最新副本时,分类仅依据 UCS 数据库、名称和 CelesTrak 分组,因此此后已失效的卫星可能仍显示为在轨工作。",
@@ -3362,17 +3362,17 @@
"SENSOR SWATH ≈ {value} KM": "传感器幅宽 ≈ {value} km",
"SENSOR": "传感器",
"Zoom to the imaging sensor swath": "缩放到成像传感器幅宽",
"<1 H OLD": "不到 1 小时前",
"{value} H OLD": "{value} 小时前",
"{value} D OLD": "{value} 天前",
"<1 H OLD": "不到1小时前",
"{value} H OLD": "{value}小时前",
"{value} D OLD": "{value}天前",
"FWD": "前方",
"AFT": "后方",
"RIGHT": "右",
"LEFT": "左",
"TRACK UP": "航迹朝上",
"TRACK UP": "航迹向上",
"COURSE HOLD · AIS": "对地航向保持 · AIS",
"Zoom in (+)": "放大(+)",
"Zoom out (−)": "缩小(−)",
"Zoom in (+)": "放大 (+)",
"Zoom out (−)": "缩小 (−)",
"ORBITAL VIEW · OFF-NADIR": "轨道视图 · 偏离天底",
"World time": "世界时间",
"Back to live": "返回实时",
@@ -3399,16 +3399,14 @@
"(+{minutes} min late)": "(晚点 {minutes} 分钟)",
"Exit cockpit to fly to a train": "请先退出座舱,再飞往列车",
"Photo © {photographer}": "照片 © {photographer}",
"Photo: {photographer} · {licence}": "照片:{photographer} · {licence}",
"Photo · {licence}": "照片 · {licence}",
"The shared flight isn't reporting now — showing where it was last seen": "分享的航班目前无位置更新——显示其最后出现的位置",
"The shared flight isn't reporting now — showing where it was at {time}": "分享的航班目前无位置更新——显示其在 {time} 的位置",
"The shared military flight isn't reporting now — showing where it was last seen": "分享的军用航班目前无位置更新——显示其最后出现的位置",
"The shared military flight isn't reporting now — showing where it was at {time}": "分享的军用航班目前无位置更新——显示其在 {time} 的位置",
"The shared vessel isn't reporting now — showing where it was last seen": "分享的船舶目前无位置更新——显示其最后出现的位置",
"The shared vessel isn't reporting now — showing where it was at {time}": "分享的船舶目前无位置更新——显示其在 {time} 的位置",
"The shared satellite isn't reporting now — showing where it was last seen": "分享的卫星目前无位置更新——显示其最后出现的位置",
"The shared satellite isn't reporting now — showing where it was at {time}": "分享的卫星目前无位置更新——显示其在 {time} 的位置",
"The shared flight isn't reporting now — showing where it was last seen": "共享的航班目前没有发送信号——显示其最后出现的位置",
"The shared flight isn't reporting now — showing where it was at {time}": "共享的航班目前没有发送信号——显示其在 {time} 的位置",
"The shared military flight isn't reporting now — showing where it was last seen": "共享的军用航班目前没有发送信号——显示其最后出现的位置",
"The shared military flight isn't reporting now — showing where it was at {time}": "共享的军用航班目前没有发送信号——显示其在 {time} 的位置",
"The shared vessel isn't reporting now — showing where it was last seen": "共享的船舶目前没有发送信号——显示其最后出现的位置",
"The shared vessel isn't reporting now — showing where it was at {time}": "共享的船舶目前没有发送信号——显示其在 {time} 的位置",
"The shared satellite isn't reporting now — showing where it was last seen": "共享的卫星目前没有发送信号——显示其最后出现的位置",
"The shared satellite isn't reporting now — showing where it was at {time}": "共享的卫星目前没有发送信号——显示其在 {time} 的位置",
"ICAO: {id}": "ICAO:{id}",
"NORAD: {id}": "NORAD:{id}",
"MMSI: {id}": "MMSI:{id}",
@@ -3442,7 +3440,7 @@
"1,000–2,000 m": "1,000–2,000 m",
"2,000–3,000 m": "2,000–3,000 m",
"3,000 m and above": "3,000 m 及以上",
"Exit cockpit to fly to a layer": "请先退出座舱,再飞往图层",
"Exit cockpit to fly to a layer": "退出驾驶舱后才能飞往图层",
"Library": "资源库",
"Environment": "环境",
"Imagery": "影像",
@@ -3477,8 +3475,8 @@
},
"switch on a dataset to load features": "开启一个数据集以加载要素",
"Switched on by this instance · part of the CCTV layer": "已由此实例开启 · 属于 CCTV 图层",
"Operator opt-in: set {env}=1 on the server": "需运营者启用:在服务器上设置 {env}=1",
"Operator opt-in: add {id} to GEV_LIBRARY_ENABLE on the server": "需运营者启用:在服务器上将 {id} 添加到 GEV_LIBRARY_ENABLE",
"Operator opt-in: set {env}=1 on the server": "运营者启用:在服务器上设置 {env}=1",
"Operator opt-in: add {id} to GEV_LIBRARY_ENABLE on the server": "运营者启用:在服务器上将 {id} 添加到 GEV_LIBRARY_ENABLE",
"Turned off by this instance's operator": "已被此实例的运营者关闭",
"Needs a provider key on the server": "需要在服务器上配置提供方密钥",
"Needs this app's server · not available in a static build": "需要此应用的服务器 · 静态构建中不可用",
+203 -205
View File
@@ -1,60 +1,60 @@
{
"News feed unavailable": "新聞源不可用",
"News feed unavailable": "新聞來源無法使用",
"Developing: {name}": "事態發展:{name}",
"{count} events today": {
"other": "今日 {count} 起事件"
},
"{count} reports today": {
"other": "今日 {count} 條報道"
"other": "今日 {count} 則報導"
},
"Disputed: {name}": "爭議地區:{name}",
"Video": "視頻",
"Video": "影片",
"Photos": "照片",
"Claimed by {who}": "{who} 聲稱",
"Claimed by {who}": "{who}聲稱",
"Claimed": "聲稱",
"Not yet confirmed by others": "尚未獲他方證實",
"Reported by {side} sources": "報道來源:{side}",
"Reported by {sides}": "報道來源:{sides}",
"{side} sources": "{side}",
"Reported by {side} sources": "{side}方來源報導",
"Reported by {sides}": "報導來源:{sides}",
"{side} sources": "{side}方來源",
"Official": "官方",
"Reported by both sides": "雙方均有報道",
"Independently reported": "有獨立來源報道",
"Reported by both sides": "雙方均有報導",
"Independently reported": "有獨立來源報導",
"Single source": "單一來源",
"International": "國際",
"Independent": "獨立",
"Unattributed": "未註明來源",
"Outside party": "第三方",
"Outside party": "外部方",
"Ukraine–Russia": "烏克蘭–俄羅斯",
"Israel–Gaza–Lebanon": "以色列–加沙–黎巴嫩",
"Iran–Hormuz": "伊朗–霍爾木茲",
"Yemen–Red Sea": "也門–紅海",
"Israel–Gaza–Lebanon": "以色列–加薩–黎巴嫩",
"Iran–Hormuz": "伊朗–荷莫茲",
"Yemen–Red Sea": "葉門–紅海",
"Sudan": "蘇丹",
"Sahel": "薩赫勒",
"Sahel": "薩赫爾",
"Elsewhere": "其他地區",
"Strike": "打擊",
"Alert": "警報",
"Air defence": "防空",
"Ground fighting": "地面戰鬥",
"Casualties": "傷亡",
"Humanitarian": "人道主義",
"Humanitarian": "人道",
"Disaster": "災害",
"Protest": "抗議",
"Political": "政治",
"No physical event": "無實際事件",
"Minor incident": "輕微事件",
"Significant": "較大",
"Major": "重大",
"Significant": "重大",
"Major": "嚴重",
"age|now": "剛剛",
"{minutes} min": "{minutes} 分鐘前",
"{hours} h": "{hours} 小時前",
"{minutes} min": "{minutes} 分鐘",
"{hours} h": "{hours} 小時",
"{count} sources": {
"other": "{count} 個來源"
},
"News feed not configured on this server": "此服務器未配置新聞源",
"Malformed news hotspots response": "新聞熱點響應格式錯誤",
"Malformed news feed response": "新聞源響應格式錯誤",
"News feed not configured on this server": "此伺服器未設定新聞來源",
"Malformed news hotspots response": "新聞熱點回應格式錯誤",
"Malformed news feed response": "新聞來源回應格式錯誤",
"{place} (placed by model)": "{place}(由模型定位)",
"English headline by the model": "模型生成的英文標題",
"English headline by the model": "模型產生的英文標題",
"Not placed on the map": "未在地圖上標出",
"Close": "關閉",
"Any severity": "任意嚴重程度",
@@ -72,16 +72,16 @@
"Hotspots": "熱點",
"Severity": "嚴重程度",
"Category": "類別",
"Nothing reported near here in the last {hours} h": "最近 {hours} 小時附近無報道",
"Nothing reported near here in the last {hours} h": "最近 {hours} 小時附近無報導",
"Latest news": "最新新聞",
"No news matches these filters in the last {hours} h": "最近 {hours} 小時沒有符合這些篩選條件的新聞",
"Load older news": "加載更早的新聞",
"Load older news": "載入更早的新聞",
"How to read the map": "如何解讀地圖",
"Colour: severity · icon: type of event": "顏色:嚴重程度 · 圖標:事件類型",
"Solid rim: reported by both sides or independently. Dashed rim: a claim. Thin rim: one source, or one side’s own press.": "實線邊框:雙方均有報道,或有獨立來源報道。虛線邊框:一方聲稱。細邊框:單一來源,或一方自身的媒體。",
"Larger: more sources · fainter: older. Picture card: a major or fast-growing event with two or more sources.": "越大:來源越多 · 越淡:越早。圖片卡片:有兩個或以上來源的重大或快速發展的事件。",
"Side labels describe the source, not whether a report is true.": "陣營標籤描述的是來源,而非報道是否屬實。",
"Exit cockpit to fly to a news report": "請先退出座艙,再飛往新聞報道地點",
"Colour: severity · icon: type of event": "顏色:嚴重程度 · 圖示:事件類型",
"Solid rim: reported by both sides or independently. Dashed rim: a claim. Thin rim: one source, or one side’s own press.": "實線邊框:雙方或獨立來源均有報導。虛線邊框:一方聲稱。細邊框:單一來源,或一方自身的媒體。",
"Larger: more sources · fainter: older. Picture card: a major or fast-growing event with two or more sources.": "越大:來源越多 · 越淡:越早。圖片卡片:有兩個或以上來源的嚴重或快速發展的事件。",
"Side labels describe the source, not whether a report is true.": "陣營標籤描述的是來源,而非報導是否屬實。",
"Exit cockpit to fly to a news report": "退出駕駛艙以飛往新聞報導",
"News items in the feed": "列表中的新聞數",
"Pick a shape, then click the map.": "先選擇形狀,再在地圖上點擊。",
"Enter to place the pin, Esc to cancel.": "按 Enter 放置圖釘,按 Esc 取消。",
@@ -407,7 +407,7 @@
"LOADING": "加載中",
"DEGRADED": "降級",
"STALE": "過期",
"SIGNAL LOST · {time}": "信號丟失 · {time}",
"SIGNAL LOST · {time}": "訊號遺失 · {time}",
"PARTIAL": "部分",
"FALLBACK": "備用",
"view band|STREET": "街道",
@@ -504,7 +504,7 @@
"Real live traffic (keyless runs a simulation)": "真實實時路況(無密鑰時為模擬)",
"Bing imagery map stacks + world terrain": "Bing 影像地圖組合 + 全球地形",
"More flight-polling credits (anonymous works without)": "更多航班輪詢額度(無密鑰也可匿名使用)",
"Street-level imagery and coverage in the Street Level layer. Free: register an app in the Mapillary developer dashboard and paste its Client Token": "街景影像與街景圖層中的覆蓋範圍。免費:在 Mapillary 開發者控制台註冊一個應用,並粘貼其 Client Token",
"Street-level imagery and coverage in the Street Level layer. Free: register an app in the Mapillary developer dashboard and paste its Client Token": "街景影像與街景圖層中的覆蓋範圍。免費:在 Mapillary 開發者控制檯註冊一個應用,並粘貼其 Client Token",
"Public space-missions access needs no key. Leave blank for standard use; paid tokens only raise request limits.": "公開的太空任務數據無需密鑰。常規使用請留空;付費令牌只會提高請求上限。",
"Air quality request failed": "空氣質量請求失敗",
"No readings available": "暫無讀數",
@@ -598,7 +598,7 @@
"FOV {value}° (DATASHEET)": "FOV {value}°(規格書)",
"FOV {value}°": "FOV {value}°",
"SPEC FOV {span}° (CAPABILITY)": "規格 FOV {span}°(可調範圍)",
"COVERAGE {value}km²": "覆蓋 {value} km²",
"COVERAGE {value}km²": "覆蓋 {value}km²",
"OVERLAP {count} cams": "重疊 {count} 個攝像頭",
"ISOLATED VIEW": "獨立視角",
"PROJ MONITOR": "投影:監視器",
@@ -615,8 +615,8 @@
"Hurricane": "颶風",
"Tropical storm": "熱帶風暴",
"Tropical depression": "熱帶低壓",
"Subtropical storm": "副熱帶風暴",
"Subtropical depression": "副熱帶低壓",
"Subtropical storm": "亞熱帶風暴",
"Subtropical depression": "亞熱帶低壓",
"Track and cone match this advisory": "路徑和預報不確定範圍與本期公告一致",
"Track/cone awaiting advisory {number}": "路徑/預報不確定範圍等待第 {number} 號公告",
"Track/cone unavailable": "路徑/預報不確定範圍不可用",
@@ -803,7 +803,7 @@
"Low-accuracy score 2–10%": "低精度評分 2–10%",
"Low-accuracy score > 10%": "低精度評分 > 10%",
"GNSS integrity source unavailable": "GNSS 完好性數據源不可用",
"Regional sample · 250 nm radius · cells from visited views over {minutes} min": "區域採樣 · 半徑 250 海里 · {minutes} 分鐘內瀏覽過的視野中的網格",
"Regional sample · 250 nm radius · cells from visited views over {minutes} min": "區域採樣 · 半徑 250 海里 · {minutes} 分鐘內已訪問視圖中的網格",
"Retrying automatically at {time} UTC": "將於 {time} UTC 自動重試",
"Retrying automatically after provider cooldown": "服務商冷卻結束後自動重試",
"Updates automatically after pan or zoom and once a minute. The public feed covers a 250-nautical-mile circle around the view centre; zooming out does not make it a worldwide snapshot. Blank cells are outside sampled coverage or have insufficient observations.": "平移或縮放後以及每分鐘自動更新。公共數據源覆蓋以視野中心為圓心、半徑 250 海里的範圍;縮小地圖並不會使其成為全球快照。空白網格位於採樣覆蓋範圍之外,或觀測數據不足。",
@@ -861,18 +861,18 @@
"{value} KM/S": "{value} km/s",
"{direction} {angle}°": "{direction} {angle}°",
"OFF-NADIR {angle}° · {offset}": "偏離天底 {angle}° · {offset}",
"SITES STANDBY": "設施待機",
"ENABLE MAPPED INSTALLATIONS FOR SITE PINGS": "啟用地圖標註設施以接收設施提醒",
"Release the camera from {name}": "讓鏡頭離開 {name}",
"SITES STANDBY": "地點待機",
"ENABLE MAPPED INSTALLATIONS FOR SITE PINGS": "啟用已測繪設施以接收地點提醒",
"Release the camera from {name}": "讓鏡頭離開{name}",
"RETURN · {name}": "返回 · {name}",
"PREVIOUS SATELLITE": "上一顆衛星",
"Return to {name}": "返回 {name}",
"Return to {name}": "返回{name}",
"SATELLITE": "衛星",
"Ride {name}": "乘坐 {name}",
"Unnamed site": "未命名設施",
"Ride {name}": "乘坐{name}",
"Unnamed site": "未命名地點",
"OFF-NADIR {angle}°": "偏離天底 {angle}°",
"FOCUS · {name}": "聚焦 · {name}",
"Keep the camera on {name}": "讓鏡頭持續對準 {name}",
"Keep the camera on {name}": "讓鏡頭持續對準{name}",
"ZOOM {zoom}×": "變焦 {zoom}×",
"SWATH {value} KM": "幅寬 {value} km",
"SWATH ≈ {value} KM": "幅寬 ≈ {value} km",
@@ -880,10 +880,10 @@
"ORBITAL VIEW · NADIR": "軌道視圖 · 天底",
"ORBITAL VELOCITY": "軌道速度",
"SUB-SATELLITE POINT": "星下點",
"NORTH UP": "正北朝上",
"CURRENTLY OVER": "當前飛越",
"{division}, {country}": "{country} {division}",
"{region} · near {place}": "{region} · {place} 附近",
"NORTH UP": "北向上",
"CURRENTLY OVER": "目前上空",
"{division}, {country}": "{division},{country}",
"{region} · near {place}": "{region} · {place}附近",
"NORAD {id}": "NORAD {id}",
"EPOCH {time}": "曆元 {time}",
"TLE": "TLE",
@@ -1163,14 +1163,14 @@
"Hybrid": "混合",
"Lines": "線條",
"Dots": "圓點",
"Road lines colored by live flow: green to red": "按實時路況著色的道路線條:從綠到紅",
"Moving dots colored by live flow": "按實時路況著色的移動圓點",
"Road lines colored by live flow: green to red": "依即時路況著色的道路線條:由綠到紅",
"Moving dots colored by live flow": "依即時路況著色的移動圓點",
"Moving dots": "移動圓點",
"TomTom key unavailable": "TomTom 密鑰不可用",
"TomTom daily budget reached": "已達 TomTom 每日配額",
"Monthly traffic allowance used": "本月交通數據配額已用完",
"Traffic rate limit reached": "已達到交通數據請求頻率上限",
"Traffic service unavailable": "交通服務不可用",
"Monthly traffic allowance used": "本月交通流量額度已用完",
"Traffic rate limit reached": "已達交通資料請求頻率上限",
"Traffic service unavailable": "交通服務無法使用",
"TomTom upstream unreachable": "TomTom 上游服務無法連接",
"TomTom flow unavailable": "TomTom 路況不可用",
"SIMULATED — {reason}": "模擬——{reason}",
@@ -1668,7 +1668,7 @@
"other": "已加載 {count} 個攝像頭 · 啟用 CCTV 以激活"
},
"{count} more cameras outside this view · pan or zoom out": {
"other": "視野外還有 {count} 個攝像頭 · 請平移或縮小地圖"
"other": "此視圖外還有 {count} 個攝像頭 · 平移或縮小"
},
"Enable CCTV to load camera intersections": "啟用 CCTV 以加載路口攝像頭",
"Enable CCTV to start camera-linked intelligence summaries.": "啟用 CCTV 以開始生成與攝像頭關聯的情報摘要。",
@@ -1694,7 +1694,7 @@
"Cycle briefing pages automatically every 9 seconds (Signals → News → Local). Pauses while you hover or focus the panel. Live signal data refreshes continuously either way.": "每 9 秒自動輪換簡報頁面(信號 → 新聞 → 本地)。懸停或聚焦面板時暫停。無論是否輪換,實時信號數據都會持續刷新。",
"ACQUIRING REGIONAL NEWS": "正在獲取地區新聞",
"REGIONAL NEWS UNAVAILABLE": "地區新聞不可用",
"CACHED · REFRESH FAILED": "緩存 · 刷新失敗",
"CACHED · REFRESH FAILED": "快取 · 重新整理失敗",
"RESOLVING REGION": "正在解析區域",
"REGION UNAVAILABLE": "區域不可用",
"NO NEWS IN THE LAST {hours} H": "過去 {hours} 小時無新聞",
@@ -1702,13 +1702,13 @@
"{value} KM/H": "{value} km/h",
"CLOUD {pct}%": "雲量 {pct}%",
"CLOUD UNKNOWN": "雲量未知",
"{source} · CACHED {age}": "{source} · 緩存 {age}",
"{source} · CACHED {age}": "{source} · 快取 {age}",
"REGIONAL NEWS": "地區新聞",
"{label} · {radius} AIR/SEA WINDOW": "{label} · {radius} 空/海監視窗口",
"{scope} · INSTALLATIONS {coverage}": "{scope} · 設施 {coverage}",
"MILITARY FEED OFF · PARTIAL PICTURE": "軍用數據源已關閉 · 態勢不完整",
"FLIGHTS FEED OFF · PARTIAL PICTURE": "航班數據源已關閉 · 態勢不完整",
"NEARBY AIRCRAFT INCOMPLETE · NOT AN ALL-CLEAR": "附近飛機信息不完整 · 不代表安全無虞",
"NEARBY AIRCRAFT INCOMPLETE · NOT AN ALL-CLEAR": "附近航空器資訊不完整 · 不代表安全無虞",
"CONTEXT STANDBY": "態勢待機",
"ENABLE GLOBAL CONTEXT FOR PROXIMITY PINGS": "啟用全局態勢以接收鄰近目標提醒",
"CONTACT LOST · LAST KNOWN READOUT · NOT AN ALL-CLEAR": "目標丟失 · 最後已知讀數 · 不代表安全無虞",
@@ -1741,8 +1741,8 @@
"ARROW · ESTIMATED DIRECTION": "箭頭 · 預估方向",
"ROUTE DATA UNAVAILABLE": "航線數據不可用",
"CONTACT NAVIGATION OFF": "目標導航已關閉",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "駕駛臺保持在所選船舶上",
"{range} NM": "{range} 海里",
"THE BRIDGE STAYS WITH THE SELECTED VESSEL": "駕駛台保持在所選船舶上",
"{range} NM": "{range}海里",
"AIS DESTINATION · CREW REPORTED": "AIS 目的地 · 船員申報",
"DEST {bearing}": "目的地 {bearing}",
"Disable cockpit weather effects": "停用座艙天氣效果",
@@ -1862,7 +1862,7 @@
"GNSS Integrity": "GNSS 完好性",
"Rain radar": "降雨雷達",
"Lightning density": "閃電密度",
"Cyclone advisories": "氣旋公告",
"Cyclone advisories": "氣旋預警",
"ALPR Cameras": "ALPR 攝像頭",
"Live Vessels": "實時船舶",
"Bike Share": "共享單車",
@@ -1887,7 +1887,7 @@
"Events": "事件",
"Weather": "天氣",
"Utilities": "工具",
"Audio": "音頻",
"Audio": "音訊",
"Other layers": "其他圖層",
"{state} · {source}": "{state} · {source}",
"UNCERTAIN · {source} · lifecycle state requires reconciliation": "不確定 · {source} · 生命週期狀態需要核對",
@@ -2063,8 +2063,8 @@
"coverage|partial": "部分",
"unknown": "未知",
"no imagery": "無影像",
"{percent}% cloud": "雲量 {percent}%",
"{min}–{max}% cloud": "雲量 {min}–{max}%",
"{percent}% cloud": "{percent}% 雲量",
"{min}–{max}% cloud": "{min}–{max}% 雲量",
"NO BOX": "無框選",
"SEARCHING": "搜索中",
"{count} DAYS": {
@@ -2576,7 +2576,7 @@
"DONE": "完成",
"HERE": "此處",
"Aircraft cockpit view": "飛機座艙視圖",
"Ship bridge view": "船舶駕駛臺視圖",
"Ship bridge view": "船舶駕駛台視圖",
"Satellite orbital view": "衛星軌道視圖",
"LOOK STBD {bearing}°": "視線 右舷 {bearing}°",
"LOOK PORT {bearing}°": "視線 左舷 {bearing}°",
@@ -2588,7 +2588,7 @@
"LOOK {elevation}": "視線 {elevation}",
"Estimated destination direction": "預估目的地方向",
"DEST ---°": "目的地 ---°",
"BRIDGE WINDOW · FWD": "駕駛臺窗口 · 前方",
"BRIDGE WINDOW · FWD": "駕駛台窗口 · 前方",
"OPTICAL PLANE · 01": "光學平面 · 01",
"HEADING HOLD · AIS": "船首向保持 · AIS",
"NO HEADING · AIS": "無船首向 · AIS",
@@ -2597,7 +2597,7 @@
"SPEED OVER GROUND · KTS": "對地航速 · kn",
"ALTITUDE · FT": "高度 · ft",
"FIRST PERSON": "第一人稱",
"BRIDGE VIEW": "駕駛臺視圖",
"BRIDGE VIEW": "駕駛台視圖",
"LIVE TRACK · COURSE ALIGNED": "實時航跡 · 航向對準",
"Cockpit vision style": "座艙視覺樣式",
"Previous cockpit vision style": "上一個座艙視覺樣式",
@@ -2606,7 +2606,7 @@
"Next cockpit vision style": "下一個座艙視覺樣式",
"Next vision style": "下一個視覺樣式",
"Current aircraft heading": "飛機當前航向",
"Look ahead: turn the bridge view back to the bow": "看向前方:將駕駛臺視圖轉回船首",
"Look ahead: turn the bridge view back to the bow": "看向前方:將駕駛台視圖轉回船首",
"Look ahead (Home or 0)": "看向前方(Home 或 0)",
"LOOK AHEAD": "看向前方",
"Current vessel heading": "船舶當前船首向",
@@ -2619,12 +2619,12 @@
"Contact cockpit summary": "目標座艙摘要",
"CONTACT": "目標",
"CONTACTS · 250 KM": "目標 · 250 km",
"RINGS {spacing} NM": "距標圈 {spacing} 海里",
"RINGS {spacing} NM": "距標圈 {spacing}海里",
"{count} CONTACTS": {
"other": "{count} 個目標"
},
"Radar range {range}. Select for the next range.": "雷達量程 {range}。選擇以切換到下一量程。",
"Bridge radar": "駕駛臺雷達",
"Bridge radar": "駕駛台雷達",
"Contact navigation": "目標導航",
"CONTEXT ONLY": "僅供參考",
"Nearby cohort counts": "附近各類目標數量",
@@ -2680,10 +2680,10 @@
"Exit cockpit and return to full globe view": "退出座艙並返回完整地球視圖",
"RESET": "重置",
"Exit cockpit view": "退出座艙視圖",
"Exit bridge view": "退出駕駛臺視圖",
"Exit bridge view": "退出駕駛台視圖",
"Exit orbital view": "退出軌道視圖",
"EXIT COCKPIT": "退出座艙",
"EXIT BRIDGE": "退出駕駛臺",
"EXIT BRIDGE": "退出駕駛台",
"EXIT ORBIT": "退出軌道視圖",
"Navigation, voice, and visual preset controls": "導航、語音和視覺預設控件",
"Expand Visual Presets": "展開視覺預設",
@@ -2708,7 +2708,7 @@
"WEATHER": "天氣",
"Active weather products": "已啟用的天氣產品",
"RECENT IMAGERY": "近期影像",
"Imagery days in the box": "框選區域內的影像天數",
"Imagery days in the box": "視野內的影像天數",
"CONTEXT": "態勢",
"Context mode": "態勢模式",
"CONTACTS": "目標",
@@ -2717,7 +2717,7 @@
"SELECT CONTEXT": "選擇態勢",
"Contact Context actions": "目標態勢操作",
"COCKPIT": "座艙",
"BRIDGE": "駕駛臺",
"BRIDGE": "駕駛台",
"SEARCH NEARBY SITES": "搜索附近設施",
"Reclassify tracked contact as TR-3B": "將跟蹤目標重新歸類為 TR-3B",
"CONTACTS CONTEXT OFF": "目標態勢已關閉",
@@ -2726,7 +2726,7 @@
"AVAILABLE MISSIONS": "可用任務",
"SELECT A MISSION TO INSPECT": "選擇一項任務以查看",
"LOADING 30-DAY MISSION INDEX": "正在加載 30 天任務索引",
"Space Missions unavailable: the mission index could not load. Try again later.": "太空任務不可用:無法加載任務索引。請稍後重試。",
"Space Missions unavailable: the mission index could not load. Try again later.": "太空任務無法使用:無法載入任務索引。請稍後再試。",
"30-DAY MISSION INDEX UNAVAILABLE": "30 天任務索引不可用",
"TAB PREVIEWS · ENTER / SPACE SELECTS": "TAB 預覽 · ENTER / 空格鍵選擇",
"Internet radio and local SDR": "網絡電臺與本地 SDR",
@@ -2984,7 +2984,7 @@
"Take the exit {direction}": "從出口{direction}駛出",
"Link shared": "鏈接已分享",
"This share link can't be opened": "無法打開此分享鏈接",
"Shared vessel is unavailable": "分享的船舶不可用",
"Shared vessel is unavailable": "分享的船隻不可用",
"Check the spelling and press Enter to search again.": "請檢查拼寫,然後按 Enter 重新搜索。",
"Press Enter to try again.": "按 Enter 重試。",
"Recent speech": "最近語音",
@@ -3078,7 +3078,7 @@
"Detection confidence": "探測置信度",
"{shown} of {total} shown": "顯示 {shown} / {total}",
"UTILITIES": "工具",
"Exit cockpit to fly to a drawing": "請先退出座艙,再飛往已繪製的形狀",
"Exit cockpit to fly to a drawing": "請先退出座艙,再飛往繪製位置",
"Directions, drawing and your own files": "路線導航、繪製和你自己的文件",
"Voice place search (needs an Overpass server)": "語音地點搜索(需要 Overpass 服務器)",
"OSM Places": "OSM 地點",
@@ -3212,64 +3212,64 @@
"Too many chat requests. Try again in {seconds} s.": "聊天請求過多。請在 {seconds} 秒後重試。",
"The provider refused access for this key. Check the key's project, region or permissions.": "服務商拒絕了此密鑰的訪問請求。請檢查該密鑰所屬的項目、區域或權限。",
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "服務商賬戶餘額不足。請檢查賬戶的計費信息,或在設置中選擇其他 AI 服務商。",
"Last 24 h of news. Each report links to its publisher; places and categories are read from the text and data automatically and can be wrong. Side labels, where shown, describe the source, not whether a report is true.": "最近 24 小時的新聞。每條報道都鏈接到其發布者;地點和類別由文本和數據自動讀取,可能有誤。陣營標籤(如有顯示)描述的是來源,而非報道是否屬實。",
"Last 24 h of news. Each report links to its publisher; places and categories are read from the text and data automatically and can be wrong. Side labels, where shown, describe the source, not whether a report is true.": "最近 24 小時的新聞。每則報導都連結至其發布者;地點與類別由文字與資料自動判讀,可能有誤。陣營標籤(如有顯示)描述的是來源,而非報導是否屬實。",
"Economy": "經濟",
"News from {area}": "{area} 新聞",
"Within {km} km of the view centre": "視野中心 {km} km 範圍內",
"{region}, {country}": "{country} {region}",
"News from {area}": "{area}新聞",
"Within {km} km of the view centre": "視圖中心 {km} 公里範圍內",
"{region}, {country}": "{country}{region}",
"via {source}": "來自 {source}",
"Local in the News layer: the day’s news from the outlets of the country in view": "新聞圖層的“本地”分區:當前視野所在國家的媒體當天的新聞",
"{provider} refused its key": "{provider} 拒絕了密鑰",
"Local in the News layer: the day’s news from the outlets of the country in view": "新聞圖層的本地:視野中國家的媒體當天的新聞",
"{provider} refused its key": "{provider} 拒絕了金鑰",
"Check {envVars} in Provider Settings": "請在服務商設置中檢查 {envVars}",
"{provider} key required": "需要 {provider} 密鑰",
"{provider} key required": "需要 {provider} 金鑰",
"{provider} limit reached for today": "{provider} 已達今日上限",
"News resumes tomorrow, or with a paid plan": "新聞將於明天恢復,或升級付費套餐",
"News key required": "需要新聞密鑰",
"News resumes tomorrow, or with a paid plan": "新聞將於明天恢復,或升級付費方案",
"News key required": "需要新聞金鑰",
"Precise conflict points in the News layer, beside GDELT’s articles": "新聞圖層中精確的衝突地點,與 GDELT 的文章並列",
"Worldwide": "全球",
"Shipwrecks": "沉船",
"Before 1900": "1900 年以前",
"After 1945": "1945 年以後",
"Before 1900": "1900年以前",
"After 1945": "1945年以後",
"Date unknown": "日期不詳",
"Position: centre of the protected area": "位置:保護區中心",
"Position approximate (within about {km} km)": "大致位置(誤差約 {km} km)",
"Position surveyed (within about {m} m)": "實測位置(誤差約 {m} m)",
"Vessel: {value}": "船型:{value}",
"Position approximate (within about {km} km)": "大致位置(誤差約 {km} 公里)",
"Position surveyed (within about {m} m)": "實測位置(誤差約 {m} 公尺)",
"Vessel: {value}": "船隻:{value}",
"What happened: {value}": "經過:{value}",
"Depth: {m} m ({quality})": "深度:{m} m({quality})",
"Depth: {m} m": "深度:{m} m",
"Depth: {m} m ({quality})": "深度:{m} 公尺({quality})",
"Depth: {m} m": "深度:{m} 公尺",
"Notes: {value}": "備註:{value}",
"Cargo or finds: {value}": "貨物或出水文物:{value}",
"Protected wreck": "受保護沉船",
"Source: {value}": "來源:{value}",
"Click the card to open EMODnet ↗": "點擊卡片打開 EMODnet ↗",
"Click the card to open the source ↗": "點擊卡片打開來源 ↗",
"Wikidata unavailable; showing EMODnet only": "Wikidata 不可用;僅顯示 EMODnet",
"EMODnet unavailable; showing Wikidata only": "EMODnet 不可用;僅顯示 Wikidata",
"Shipwrecks unavailable": "沉船數據不可用",
"Click the card to open EMODnet ↗": "點擊卡片開啟 EMODnet ↗",
"Click the card to open the source ↗": "點擊卡片開啟來源 ↗",
"Wikidata unavailable; showing EMODnet only": "Wikidata 無法使用;僅顯示 EMODnet",
"EMODnet unavailable; showing Wikidata only": "EMODnet 無法使用;僅顯示 Wikidata",
"Shipwrecks unavailable": "沉船資料無法使用",
"EMODnet (European waters) · Wikidata (worldwide)": "EMODnet(歐洲海域)· Wikidata(全球)",
"{count} wrecks": {
"other": "{count} 艘沉船"
},
"1900–1945": "1900–1945",
"Shipwreck": "沉船",
"Fatalities: {count}": "遇難人數:{count}",
"Survivors: {count}": "倖存人數:{count}",
"Click the card to open the article ↗": "點擊卡片打開文章 ↗",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "歐洲歷史沉船來自 EMODnet Human Activities;全球知名沉船來自 Wikidata。顏色表示船隻沉沒的時期,標記大小隨遇難人數增加。點擊標記查看卡片。",
"Fatalities: {count}": "罹難:{count}",
"Survivors: {count}": "倖存者:{count}",
"Click the card to open the article ↗": "點擊卡片開啟文章 ↗",
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "歐洲歷史沉船來自 EMODnet Human Activities;全球知名沉船來自 Wikidata。顏色表示船隻沉沒的時期,標記大小隨罹難人數增加。點擊標記查看卡片。",
"All years": "所有年份",
"Period": "時段",
"From": "起",
"To": "止",
"To": "迄",
"First year of the shipwreck period": "沉船時段的起始年份",
"Last year of the shipwreck period": "沉船時段的結束年份",
"SIGHTING": "目擊",
"Show {name} sighting": "顯示 {name} 的目擊記錄",
"Show {name} sighting": "顯示{name}目擊",
"seen {age} ago": "{age}前目擊",
"observed {age} ago": "{age}前觀察到",
"Be Whale Wise": "負責任地觀鯨",
"Reported by {name} ({entity})": "報告方:{name}({entity})",
"Reported by {name}": "報告方:{name}",
"observed {age} ago": "{age}前觀察",
"Be Whale Wise": "負責任地賞鯨",
"Reported by {name} ({entity})": "通報方 {name}({entity})",
"Reported by {name}": "通報方 {name}",
"Salish Sea": "薩利希海",
"Birds": "鳥類",
"Mammals": "哺乳類",
@@ -3282,18 +3282,18 @@
"Fungi": "真菌",
"Other animals": "其他動物",
"Other or unknown": "其他或未知",
"{label} {detail} — click to retry": "{label} {detail}——點擊重試",
"Wildlife sightings source unavailable": "野生生物目擊來源不可用",
"Show only community-confirmed (research-grade) sightings": "僅顯示社區確認(研究級)的目擊",
"{label} {detail} — click to retry": "{label} {detail} — 點擊重試",
"Wildlife sightings source unavailable": "野生生物目擊來源無法使用",
"Show only community-confirmed (research-grade) sightings": "僅顯示社群確認(研究級)的目擊",
"No recent sightings in view": "視野內無近期目擊",
"Acartia feed unavailable": "Acartia 數據源不可用",
"Acartia feed unavailable": "Acartia資料來源無法使用",
"Show all taxa": "顯示所有分類群",
"BIRDS": "鳥類",
"Show only birds": "僅顯示鳥類",
"MAMMALS": "哺乳類",
"Show only mammals": "僅顯示哺乳類",
"WHALES": "鯨類",
"Show only whales and dolphins (7 days)": "僅顯示鯨和海豚(7 天)",
"Show only whales and dolphins (7 days)": "僅顯示鯨和海豚(7天)",
"INSECTS": "昆蟲",
"Show only insects": "僅顯示昆蟲",
"PLANTS": "植物",
@@ -3301,87 +3301,87 @@
"All rights reserved": "保留所有權利",
"Cetacean": "鯨類",
"Wildlife Sightings": "野生生物目擊",
"Exit cockpit to fly to a sighting": "請先退出座艙,再飛往目擊地點",
"Exit cockpit to fly to a sighting area": "請先退出座艙,再飛往目擊區域",
"Exit cockpit to fly to a sighting": "退出駕駛艙以飛往目擊地點",
"Exit cockpit to fly to a sighting area": "退出駕駛艙以飛往目擊區域",
"Wildlife": "野生生物",
"Zoom in for local sightings": "放大查看當地目擊",
"Loading photo...": "正在加載照片…",
"Photo unavailable": "照片不可用",
"Location obscured to a 0.2° cell": "位置已模糊至 0.2° 網格",
"Photo of {name}": "{name} 的照片",
"Loading photo...": "正在載入照片…",
"Photo unavailable": "照片無法使用",
"Location obscured to a 0.2° cell": "位置已模糊至0.2°網格",
"Photo of {name}": "{name}的照片",
"Acartia": "Acartia",
"CC BY 4.0": "CC BY 4.0",
"{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°。並非種群普查。",
"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}",
"Open {name} on Acartia": "在 Acartia 上打開 {name}",
"iNaturalist": "iNaturalist",
"iNaturalist contributors · licences per record": "iNaturalist 貢獻者 · 各記錄單獨授權",
"iNaturalist contributors · Acartia {count} (Salish Sea) · licences per record": "iNaturalist 貢獻者 · Acartia {count}(薩利希海)· 各記錄單獨授權",
"{days}d": "{days} 天",
"iNaturalist contributors · licences per record": "iNaturalist 貢獻者 · 各紀錄個別授權",
"iNaturalist contributors · Acartia {count} (Salish Sea) · licences per record": "iNaturalist 貢獻者 · Acartia {count}(薩利希海) · 各紀錄個別授權",
"{days}d": "{days}天",
"All markers": "所有標記",
"Show markers of every age": "顯示所有時間的標記",
"Maximum event age: {days}d; undated markers are kept": "僅顯示 {days} 天內的事件;保留無日期標記",
"Maximum event age: {days}d; undated markers are kept": "事件最長保留時間:{days}天;保留無日期標記",
"event age filter|All": "全部",
"Event age": "事件時間",
"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 地址。",
"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": "視野中心",
"the view centre": "視圖中心",
"{source} at {place}": "{place} 的 {source}",
"Street level at {place}": "{place} 的街景",
"Switching map": "正在切換地圖",
"Opening imagery": "正在打開影像",
"Street View is ready: press {open} to load it": "街景已就緒:點擊“{open}”加載",
"Street View is ready: press {open} to load it": "街景已就緒:按 {open} 加載",
"{source} cannot open this link's street-level photo without a working key": "沒有有效密鑰,{source} 無法打開此鏈接的街景照片",
"Street-level photos are hidden in the current view. Leave it first.": "當前視圖隱藏了街景照片。請先退出該視圖。",
"STATIONS": "空間站",
"Crewed stations and the craft that visit them": "載人空間站及來訪飛行器",
"STATIONS": "太空站",
"Crewed stations and the craft that visit them": "載人太空站及來訪飛行器",
"IMAGING": "成像",
"Earth observation: optical, radar, weather and science": "對地觀測:光學、雷達、氣象與科學",
"TELECOM": "通信",
"Communications and navigation satellites, Starlink excepted": "通信與導航衛星(不含 Starlink)",
"TELECOM": "通訊",
"Communications and navigation satellites, Starlink excepted": "通訊與導航衛星(不含 Starlink)",
"STARLINK": "STARLINK",
"The Starlink constellation (thousands of extra points)": "Starlink 星座(數千個額外點位)",
"PROBES": "探測器",
"Space science, technology demonstration and other payloads": "空間科學、技術驗證及其他載荷",
"Space science, technology demonstration and other payloads": "太空科學、技術驗證及其他酬載",
"DEBRIS": "碎片",
"Rocket bodies, fragments and dead satellites": "火箭箭體、碎片和失效衛星",
"Classes may lag: SATCAT is a cached copy": "分類可能滯後:SATCAT 為緩存副本",
"Classes may lag: SATCAT unavailable": "分類可能滯後:SATCAT 不可用",
"CelesTrak SATCAT gives each object's type and status. Without a current copy the classes rest on the UCS database, names and CelesTrak groups, so a satellite that has died since may still show as working.": "CelesTrak SATCAT 給出每個物體的類型和運行狀態。沒有最新副本時,分類僅依據 UCS 數據庫、名稱和 CelesTrak 分組,因此此後已失效的衛星可能仍顯示為在軌工作。",
"Rocket bodies, fragments and dead satellites": "火箭殘骸、碎片與失效衛星",
"Classes may lag: SATCAT is a cached copy": "分類可能落後:SATCAT 為快取副本",
"Classes may lag: SATCAT unavailable": "分類可能落後:SATCAT 無法取得",
"CelesTrak SATCAT gives each object's type and status. Without a current copy the classes rest on the UCS database, names and CelesTrak groups, so a satellite that has died since may still show as working.": "CelesTrak SATCAT 提供每個物體的類型與運作狀態。沒有最新副本時,分類僅依據 UCS 資料庫、名稱與 CelesTrak 群組,因此此後已失效的衛星可能仍顯示為運作中。",
"STARLINK ···": "STARLINK ···",
"STARLINK ✕": "STARLINK ✕",
"SENSOR SWATH {value} KM": "傳感器幅寬 {value} km",
"SENSOR SWATH ≈ {value} KM": "傳感器幅寬 ≈ {value} km",
"SENSOR": "傳感器",
"Zoom to the imaging sensor swath": "縮放到成像傳感器幅寬",
"<1 H OLD": "不到 1 小時前",
"{value} H OLD": "{value} 小時前",
"{value} D OLD": "{value} 天前",
"SENSOR SWATH {value} KM": "感測器幅寬 {value} km",
"SENSOR SWATH ≈ {value} KM": "感測器幅寬 ≈ {value} km",
"SENSOR": "感測器",
"Zoom to the imaging sensor swath": "縮放到成像感測器幅寬",
"<1 H OLD": "不到1小時前",
"{value} H OLD": "{value}小時前",
"{value} D OLD": "{value}天前",
"FWD": "前方",
"AFT": "後方",
"RIGHT": "右",
"LEFT": "左",
"TRACK UP": "航跡朝上",
"TRACK UP": "航跡向上",
"COURSE HOLD · AIS": "對地航向保持 · AIS",
"Zoom in (+)": "放大(+)",
"Zoom out (−)": "縮小(−)",
"Zoom in (+)": "放大 (+)",
"Zoom out (−)": "縮小 (−)",
"ORBITAL VIEW · OFF-NADIR": "軌道視圖 · 偏離天底",
"World time": "世界時間",
"Back to live": "返回實時",
"Live": "實時",
"Back to live": "返回即時",
"Live": "即時",
"1 h/s": "1 小時/秒",
"1 d/s": "1 天/秒",
"1 wk/s": "1 週/秒",
"Play speed": "播放速度",
"Scroll or pinch to zoom, drag to move along · double-click to see it all": "滾動或雙指縮放,拖動以移動 · 雙擊查看全部",
"Scroll or pinch to zoom, drag to move along · double-click to see it all": "捲動或雙指縮放,拖曳以移動 · 按兩下查看全部",
"1 min/s": "1 分鐘/秒",
"Rail": "鐵路",
"Urban rail": "城市軌道",
@@ -3391,24 +3391,22 @@
"International rail": "國際鐵路",
"Hide {category}": "隱藏{category}",
"Show {category}": "顯示{category}",
"Live GPS": "實時 GPS",
"Live GPS": "即時 GPS",
"Estimated from station reports": "根據車站報告估算",
"To {headsign}": "開往 {headsign}",
"Next {stop} {time} {relative}": "下一站 {stop} {time} {relative}",
"Prev {stop} {time} {relative}": "上一站 {stop} {time} {relative}",
"(+{minutes} min late)": "(晚點 {minutes} 分鐘)",
"(+{minutes} min late)": "(誤點 {minutes} 分鐘)",
"Exit cockpit to fly to a train": "請先退出座艙,再飛往列車",
"Photo © {photographer}": "照片 © {photographer}",
"Photo: {photographer} · {licence}": "照片:{photographer} · {licence}",
"Photo · {licence}": "照片 · {licence}",
"The shared flight isn't reporting now — showing where it was last seen": "分享的航班目前無位置更新——顯示其最後出現的位置",
"The shared flight isn't reporting now — showing where it was at {time}": "分享的航班目前無位置更新——顯示其在 {time} 的位置",
"The shared military flight isn't reporting now — showing where it was last seen": "分享的軍用航班目前無位置更新——顯示其最後出現的位置",
"The shared military flight isn't reporting now — showing where it was at {time}": "分享的軍用航班目前無位置更新——顯示其在 {time} 的位置",
"The shared vessel isn't reporting now — showing where it was last seen": "分享的船舶目前無位置更新——顯示其最後出現的位置",
"The shared vessel isn't reporting now — showing where it was at {time}": "分享的船舶目前無位置更新——顯示其在 {time} 的位置",
"The shared satellite isn't reporting now — showing where it was last seen": "分享的衛星目前無位置更新——顯示其最後出現的位置",
"The shared satellite isn't reporting now — showing where it was at {time}": "分享的衛星目前無位置更新——顯示其在 {time} 的位置",
"The shared flight isn't reporting now — showing where it was last seen": "共享的航班目前沒有發送信號——顯示其最後出現的位置",
"The shared flight isn't reporting now — showing where it was at {time}": "共享的航班目前沒有發送信號——顯示其在 {time} 的位置",
"The shared military flight isn't reporting now — showing where it was last seen": "共享的軍用航班目前沒有發送信號——顯示其最後出現的位置",
"The shared military flight isn't reporting now — showing where it was at {time}": "共享的軍用航班目前沒有發送信號——顯示其在 {time} 的位置",
"The shared vessel isn't reporting now — showing where it was last seen": "共享的船舶目前沒有發送信號——顯示其最後出現的位置",
"The shared vessel isn't reporting now — showing where it was at {time}": "共享的船舶目前沒有發送信號——顯示其在 {time} 的位置",
"The shared satellite isn't reporting now — showing where it was last seen": "共享的衛星目前沒有發送信號——顯示其最後出現的位置",
"The shared satellite isn't reporting now — showing where it was at {time}": "共享的衛星目前沒有發送信號——顯示其在 {time} 的位置",
"ICAO: {id}": "ICAO:{id}",
"NORAD: {id}": "NORAD:{id}",
"MMSI: {id}": "MMSI:{id}",
@@ -3428,61 +3426,61 @@
"{place} has no position to look for imagery at": "{place} 沒有可查找影像的位置",
"No {source} imagery near {place}": "{place} 附近沒有 {source} 影像",
"{reason} (filters on: {filters})": "{reason}(已開啟篩選:{filters})",
"tiles unavailable": "瓦片不可用",
"zoom in to load this layer": "放大以加載此圖層",
"tiles unavailable": "圖磚無法使用",
"zoom in to load this layer": "放大以載入此圖層",
"Zoom to layer": "縮放至圖層",
"Zoom to layer · fly to where this layer has data": "縮放至圖層 · 飛往此圖層有數據的區域",
"Zoom to layer · fly to where this layer has data": "縮放至圖層 · 飛往此圖層有資料的區域",
"Remove from Data Layers": "從數據圖層中移除",
"Remove from Data Layers · add it again from the library": "從數據圖層中移除 · 可從數據資源庫重新添加",
"Coverage outline": "覆蓋範圍輪廓",
"Remove from Data Layers · add it again from the library": "從數據圖層中移除 · 可從數據資源庫重新加入",
"Coverage outline": "涵蓋範圍輪廓",
"DRAPED": "貼地",
"Below 250 m": "低於 250 m",
"250–500 m": "250–500 m",
"500–1,000 m": "500–1,000 m",
"1,000–2,000 m": "1,000–2,000 m",
"2,000–3,000 m": "2,000–3,000 m",
"3,000 m and above": "3,000 m 及以上",
"Exit cockpit to fly to a layer": "請先退出座艙,再飛往圖層",
"3,000 m and above": "3,000 m 以上",
"Exit cockpit to fly to a layer": "離開駕駛艙後才能飛往圖層",
"Library": "資源庫",
"Environment": "環境",
"Imagery": "影像",
"Demographics": "人口統計",
"DATASET": "數據集",
"TILES": "瓦片",
"PACK": "數據包",
"DATASET": "資料集",
"TILES": "圖磚",
"PACK": "資料包",
"INCUBATING": "孵化中",
"COMMUNITY": "社區",
"COMMUNITY": "社群",
"VERIFIED": "已驗證",
"ARCHIVED": "已歸檔",
"Datasets": "數據集",
"ARCHIVED": "已封存",
"Datasets": "資料集",
"{datasets} · {tiles} · {packs}": "{datasets} · {tiles} · {packs}",
"{count} datasets": {
"other": "{count} 個數據集"
"other": "{count} 個資料集"
},
"{count} tile layers": {
"other": "{count} 個瓦片圖層"
"other": "{count} 個圖磚圖層"
},
"{count} packs": {
"other": "{count} 個數據包"
"other": "{count} 個資料包"
},
"Themes": "主題",
"Added": "已添加",
"Added": "已加入",
"{count} on the map": {
"other": "地圖上 {count} 個"
},
"none yet": "暫無",
"Map features": "地圖要素",
"none yet": "尚無",
"Map features": "地圖圖徵",
"from {count} datasets": {
"other": "來自 {count} 個數據集"
"other": "來自 {count} 個資料集"
},
"switch on a dataset to load features": "開啟一個數據集以加載要素",
"Switched on by this instance · part of the CCTV layer": "已由此實例開啟 · 屬於 CCTV 圖層",
"Operator opt-in: set {env}=1 on the server": "需運營者啟用:在服務器上設置 {env}=1",
"Operator opt-in: add {id} to GEV_LIBRARY_ENABLE on the server": "需運營者啟用:在服務器上將 {id} 添加到 GEV_LIBRARY_ENABLE",
"Turned off by this instance's operator": "已被此實例的運營者關閉",
"Needs a provider key on the server": "需要在服務器上配置提供方密鑰",
"Needs this app's server · not available in a static build": "需要此應用的服務器 · 靜態構建中不可用",
"Checking what this instance serves…": "正在檢查此實例提供的內容…",
"switch on a dataset to load features": "開啟一個資料集以載入圖徵",
"Switched on by this instance · part of the CCTV layer": "已由此執行個體開啟 · 屬於 CCTV 圖層",
"Operator opt-in: set {env}=1 on the server": "營運者啟用:在伺服器上設定 {env}=1",
"Operator opt-in: add {id} to GEV_LIBRARY_ENABLE on the server": "營運者啟用:在伺服器上將 {id} 加入 GEV_LIBRARY_ENABLE",
"Turned off by this instance's operator": "已被此執行個體的營運者關閉",
"Needs a provider key on the server": "需要在伺服器上設定提供者金鑰",
"Needs this app's server · not available in a static build": "需要此應用程式的伺服器 · 靜態建置中無法使用",
"Checking what this instance serves…": "正在檢查此執行個體提供的內容…",
"Regional": "區域",
"Orthophoto": "正射影像",
"Satellite mosaic": "衛星鑲嵌圖",
@@ -3490,33 +3488,33 @@
"Historic imagery": "歷史影像",
"True orthophoto": "真正射影像",
"Type": "類型",
"Resolution": "分辨率",
"Flown": "航攝時間",
"Resolution": "解析度",
"Flown": "航拍時間",
"Updates": "更新頻率",
"Ortho quality": "正射質量",
"Ortho quality": "正射品質",
"DATA LIBRARY": "數據資源庫",
"Close data library": "關閉數據資源庫",
"Data Library at a glance": "數據資源庫概覽",
"Live sources beyond the core layers. Each one is fetched from its provider while it is on; nothing is stored in this app. Add one and it appears in Data Layers under Library.": "核心圖層之外的實時數據源。每個數據源開啟時直接從其提供方獲取;本應用不存儲任何數據。添加後,它會出現在數據圖層的“資源庫”分組中。",
"Name, provider or country": "名稱、提供方或國家",
"Live sources beyond the core layers. Each one is fetched from its provider while it is on; nothing is stored in this app. Add one and it appears in Data Layers under Library.": "核心圖層以外的即時資料來源。每個來源開啟時直接從其提供者取得;本應用程式不儲存任何資料。加入後,它會出現在數據圖層的「資源庫」分組中。",
"Name, provider or country": "名稱、提供者或國家",
"Theme": "主題",
"All themes": "全部主題",
"Only what this instance serves": "僅顯示此實例提供的內容",
"All themes": "所有主題",
"Only what this instance serves": "僅顯示此執行個體提供的內容",
"OPT-IN": "需啟用",
"KEY": "需密鑰",
"KEYLESS": "免密鑰",
"KEY": "需金鑰",
"KEYLESS": "免金鑰",
"ZOOM": "縮放",
"Zoom to {name}": "縮放至 {name}",
"WORKING…": "處理中…",
"ADD": "添加",
"ADD": "加入",
"Remove {name} from Data Layers": "從數據圖層中移除 {name}",
"Add {name} to Data Layers": "將 {name} 添加到數據圖層",
"Add {name} to Data Layers": "將 {name} 加入數據圖層",
"CONFIRM · REMOVE ALL {count}": {
"other": "確認 · 全部移除 {count} 項"
},
"REMOVE ALL": "全部移除",
"Confirm: remove every added entry from Data Layers": "確認:從數據圖層中移除所有已添加條目",
"Remove every added entry from Data Layers": "從數據圖層中移除所有已添加條目",
"Confirm: remove every added entry from Data Layers": "確認:從數據圖層中移除所有已加入項目",
"Remove every added entry from Data Layers": "從數據圖層中移除所有已加入項目",
"Showing {shown} of {total}": "顯示 {shown} / {total}",
"BROWSE DATA LIBRARY": "瀏覽數據資源庫"
}
-81
View File
@@ -1,81 +0,0 @@
/**
* @module i18n/page
* @description The page's interface language, for an application that
* composes Core and offers its own language choice (a menu item, a settings
* page) instead of the globe picker. It starts the language the standalone
* application starts, keeps `<html lang>`/`<html dir>` and the page's
* `data-i18n` markup in step, and switches in place, stored for the next
* visit, exactly as the picker does. Guide: docs/I18N.md.
*/
import {
DEFAULT_LOCALE,
LOCALES,
LOCALE_NAMES,
applyDocumentLanguage,
isRtlLocale,
normalizeLocale,
resolveLocale,
} from './locale.js';
import {
applyDocumentTranslations,
getLocale,
onLocaleChange,
persistLocale,
setLocale,
} from './index.js';
export {
DEFAULT_LOCALE,
LOCALES,
LOCALE_NAMES,
applyDocumentTranslations,
getLocale,
isRtlLocale,
onLocaleChange,
};
/**
* Start the page in its language: `?lang=`, then the stored choice, then the
* browser's languages, then English. Call it before any view renders, so
* every lookup and `<html lang>`/`<html dir>` agree from the first frame; the
* page's static `data-i18n` markup (a loading screen) is translated now.
* @param {Document} [doc]
* @param {Parameters<typeof resolveLocale>[0]} [inputs] Location, storage
* and languages, for tests.
* @returns {string} The active locale.
*/
export function startPageLocale(doc = globalThis.document, inputs) {
const locale = setLocale(resolveLocale(inputs));
applyDocumentLanguage(doc, locale);
applyDocumentTranslations(doc);
return locale;
}
/**
* Keep `<html lang>`/`<html dir>` and the page's `data-i18n` markup in step
* with every language switch.
* @param {Document} [doc]
* @returns {() => void} Stops following.
*/
export function followPageLocale(doc = globalThis.document) {
return onLocaleChange(() => {
applyDocumentLanguage(doc, getLocale());
applyDocumentTranslations(doc);
});
}
/**
* Switch the page to `locale` in place and keep it for the next visit. The
* active language, or an unknown code that would fall back to it, changes
* nothing.
* @param {string} locale One of LOCALES.
* @param {Parameters<typeof persistLocale>[1]} [options] Location, history
* and storage, for tests.
* @returns {'unchanged'|'switched'}
*/
export function switchLocale(locale, options) {
const next = normalizeLocale(locale) || DEFAULT_LOCALE;
if (next === getLocale()) return 'unchanged';
setLocale(persistLocale(next, options));
return 'switched';
}
-106
View File
@@ -1,106 +0,0 @@
// src/i18n/page.test.mjs — an application with its own language choice starts,
// follows and switches the page's language as the globe picker does.
import { afterEach, test } from 'node:test';
import assert from 'node:assert/strict';
import { LOCALE_STORAGE_KEY } from './locale.js';
import {
followPageLocale,
getLocale,
startPageLocale,
switchLocale,
} from './page.js';
import { setLocale } from './index.js';
afterEach(() => setLocale('en'));
function memoryStorage() {
const data = new Map();
return {
data,
getItem: (key) => data.get(key) ?? null,
setItem: (key, value) => data.set(key, String(value)),
};
}
function page(elements = []) {
return {
documentElement: { lang: '', dir: '' },
querySelectorAll: (selector) =>
selector === '[data-i18n]' ? elements : [],
};
}
/** A data-i18n element, as applyDocumentTranslations reads one. */
function marked(text) {
return {
textContent: text,
hasAttribute: (name) => name === 'data-i18n',
getAttribute: () => null,
};
}
test('the page starts in ?lang=, else the stored choice, with lang and dir set', () => {
const doc = page();
const storage = memoryStorage();
storage.setItem(LOCALE_STORAGE_KEY, 'de');
assert.equal(
startPageLocale(doc, {
location: { search: '?lang=ar' },
storage,
languages: ['ru'],
}),
'ar',
);
assert.equal(getLocale(), 'ar');
assert.deepEqual(doc.documentElement, { lang: 'ar', dir: 'rtl' });
assert.equal(
startPageLocale(doc, { location: { search: '' }, storage, languages: [] }),
'de',
);
assert.deepEqual(doc.documentElement, { lang: 'de', dir: 'ltr' });
});
test("the page's data-i18n markup is in its language from the start", () => {
const close = marked('Close');
startPageLocale(page([close]), {
location: { search: '?lang=de' },
storage: memoryStorage(),
languages: [],
});
assert.equal(close.textContent, 'Schließen');
});
test('a switch is stored, drops ?lang, and the followed page takes it', () => {
const doc = page();
const stop = followPageLocale(doc);
const storage = memoryStorage();
const replaced = [];
const options = {
location: { href: 'https://example.test/?lang=fr#view' },
history: { replaceState: (_s, _t, url) => replaced.push(url) },
storage,
};
assert.equal(switchLocale('he', options), 'switched');
assert.equal(getLocale(), 'he');
assert.equal(storage.data.get(LOCALE_STORAGE_KEY), 'he');
assert.deepEqual(replaced, ['https://example.test/#view']);
assert.deepEqual(doc.documentElement, { lang: 'he', dir: 'rtl' });
stop();
switchLocale('ja', options);
assert.deepEqual(doc.documentElement, { lang: 'he', dir: 'rtl' });
});
test('choosing the active language, or one that falls back to it, changes nothing', () => {
const storage = memoryStorage();
const options = {
location: { href: 'https://example.test/' },
history: { replaceState() {} },
storage,
};
assert.equal(switchLocale('en', options), 'unchanged');
assert.equal(switchLocale('xx', options), 'unchanged');
assert.equal(storage.data.size, 0);
assert.equal(switchLocale('pt-BR', options), 'switched');
assert.equal(getLocale(), 'pt');
assert.equal(switchLocale('pt', options), 'unchanged');
});
+2 -6
View File
@@ -247,9 +247,7 @@
"NORAD: {id}",
"COMMUNITY",
"Regional",
"OPT-IN",
"{minutes} min",
"1 min/s"
"OPT-IN"
],
"es": [
"1 min/s",
@@ -627,13 +625,11 @@
"▲ {frp} MW",
"SATELLITE",
"Photo © {photographer}",
"Photo · {licence}",
"PACK",
"{count} packs",
"Orthophoto",
"Oblique",
"Type",
"{range} NM"
"Type"
],
"it": [
"1 min/s",
+5 -17
View File
@@ -182,22 +182,7 @@ export function createPanel({ state: layerState, parts }) {
: null;
}
/** The credit line: the photographer, and an open photo's licence. A
* photo whose answer names no photographer shows no photographer. */
function photoCredit(photo) {
if (photo.licence)
return photo.photographer
? t('Photo: {photographer} · {licence}', {
photographer: photo.photographer,
licence: photo.licence,
})
: t('Photo · {licence}', { licence: photo.licence });
return photo.photographer
? t('Photo © {photographer}', { photographer: photo.photographer })
: '';
}
/** The subject's photo with its credit, or null. The subject
/** The tracked airliner's photo with its credit, or null. The subject
* refresh resolves it; only the subject it was resolved for shows it. */
function presentPhoto(subject) {
const held = layerState.subjectPhoto;
@@ -213,7 +198,10 @@ export function createPanel({ state: layerState, parts }) {
width: photo.width ?? null,
height: photo.height ?? null,
link: photo.link,
credit: photoCredit(photo),
// A photo whose answer names no photographer shows no credit line.
credit: photo.photographer
? t('Photo © {photographer}', { photographer: photo.photographer })
: '',
};
}
+1 -46
View File
@@ -113,52 +113,7 @@ test('the Contacts view has no photo without one, or with another subject’s',
);
});
test('the selected vessel shows its own photo, by its id', () => {
const asked = [];
const { resolveSubjectPhoto, subjectFromContext } = createSubject({
state: {},
services: {
flights: {},
military: {},
vessels: {
getVesselPhoto: (id) => {
asked.push(id);
return id === 'uuid-1' ? PHOTO : null;
},
},
installations: {},
},
parts: { navigation: { isFlightLayer: () => false } },
});
assert.deepEqual(
resolveSubjectPhoto({ layerId: 'ais-live-vessels', id: 'uuid-1' }),
PHOTO,
);
assert.equal(
resolveSubjectPhoto({ layerId: 'ais-live-vessels', id: 'uuid-2' }),
null,
);
assert.deepEqual(asked, ['uuid-1', 'uuid-2']);
const vessel = (label, mmsi) =>
subjectFromContext({
layerId: 'ais-live-vessels',
id: 'ais-uuid-1',
label,
latitude: 38.3,
longitude: -123.05,
properties: { vesselId: 'uuid-1', mmsi },
});
assert.equal(
vessel('USCGC SOCKEYE', '366999709').label,
'USCGC SOCKEYE · MMSI 366999709',
);
assert.equal(vessel('USCGC SOCKEYE', '366999709').id, 'uuid-1');
// A vessel named by its MMSI, or without one, is not given it twice.
assert.equal(vessel('MMSI 366999709', '366999709').label, 'MMSI 366999709');
assert.equal(vessel('USCGC SOCKEYE', null).label, 'USCGC SOCKEYE');
});
test('only the tracked flight has a flight photo: not an installation, a military aircraft or another flight', () => {
test('only the tracked flight has a photo: not a vessel, an installation, a military aircraft or another flight', () => {
const tracked = {
layerId: 'flights',
id: 'abc123',
+6 -14
View File
@@ -406,20 +406,17 @@ export function createSubject({ state: layerState, services, parts, source }) {
}
/**
* The subject's photo, for the Contacts panel: the tracked airliner's
* Planespotters photo, or the selected vessel's Open Vessel Photos photo.
* The tracked airliner's Planespotters photo, for the Contacts panel.
*
* The flights layer looks photos up only for the plane it tracks, the
* vessel layer only for the vessel selected; an installation, a military
* aircraft or an untracked flight has none.
* Only the flights layer looks photos up, and only for the plane it tracks;
* a vessel, an installation, a military aircraft or an untracked flight has
* none.
* @param {{layerId: string, id: string}|null} subject Current subject.
* @param {?object} [tracked] The subject's tracked descriptor, when the
* caller already has it.
* @returns {?{src: string, width: ?number, height: ?number, photographer: string, link: string, licence?: string}}
* @returns {?{src: string, width: ?number, height: ?number, photographer: string, link: string}}
*/
function resolveSubjectPhoto(subject, tracked = trackedSubjectOf(subject)) {
if (subject?.layerId === 'ais-live-vessels')
return aisLiveVesselsLayer.getVesselPhoto?.(subject.id) ?? null;
return subject?.layerId === 'flights' && tracked?.photo
? tracked.photo
: null;
@@ -520,15 +517,10 @@ export function createSubject({ state: layerState, services, parts, source }) {
const latitude = Number(record.latitude);
const longitude = Number(record.longitude);
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null;
// A vessel reads as its name and MMSI; a vessel named by its MMSI alone
// already carries it.
const label = String(record.label || record.id);
const mmsi = String(record.properties?.mmsi || '');
return {
layerId: record.layerId,
id: record.properties?.vesselId || record.id,
// i18n-ignore: MMSI is a radio identity code, as on the vessel card
label: mmsi && !label.includes(mmsi) ? `${label} · MMSI ${mmsi}` : label,
label: record.label || record.id,
memberNames: record.properties?.memberNames || [],
position: Cesium.Cartesian3.fromDegrees(longitude, latitude, 0),
};
@@ -0,0 +1,331 @@
// Station ground heights. A city's stations are all drawn at once at the
// fallback height; their ground heights are found afterwards a few
// milliseconds at a time, so a city of hundreds of docks never holds the main
// thread for the length of hundreds of depth renders. Each case drives the
// real rendering part (and the real lifecycle where it tears down) over a
// scene whose height reads are counted and timed.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { createState } from './state.js';
import { createModel } from './model.js';
import { createRendering } from './rendering.js';
import { createLifecycle } from './lifecycle.js';
import { POINT_HEIGHT_OFFSET_M } from './policy.js';
import { createTerrainSurface } from '../../services/terrainSurface.js';
globalThis.document ??= {
addEventListener() {},
removeEventListener() {},
};
/** Milliseconds one fake depth render holds the thread. */
const SAMPLE_COST_MS = 1;
/** The ground under a point, as the fake scene and globe both report it. */
const groundAt = (lat, lon) => 100 + (lat - 42) * 1000 + (lon + 71) * 10;
function busyWait(ms) {
const until = performance.now() + ms;
while (performance.now() < until);
}
/** `count` stations on a line through Boston. */
function stations(count, prefix = 's') {
return {
data: {
stations: Array.from({ length: count }, (_, i) => ({
station_id: `${prefix}${i}`,
name: `Dock ${i}`,
lat: 42.3 + i * 1e-4,
lon: -71.1 + i * 1e-4,
capacity: 10,
})),
},
};
}
/**
* The rendering part over a scene that counts its height reads.
* @param {{terrainGlobe?: boolean, shared?: boolean, unloaded?: (lat: number) => boolean}} options
* `terrainGlobe`: the globe is showing (else photorealistic tiles hide it);
* `shared`: the application's terrain surface is supplied;
* `unloaded`: latitudes whose terrain tile is not drawn.
*/
function makeLayer({
terrainGlobe = false,
shared = false,
unloaded = () => false,
} = {}) {
const calls = { sampleHeight: 0, getHeight: 0, sharedHeight: 0, render: 0 };
// sampleHeight calls in this turn of the event loop, and the most in any
// turn so far. Two numbers, not a list: a slow run has a great many turns.
let thisTurn = 0;
let mostInATurn = 0;
let ticking = true;
const tick = () => {
if (!ticking) return;
thisTurn = 0;
setImmediate(tick);
};
setImmediate(tick);
const degrees = (carto) => [
Cesium.Math.toDegrees(carto.latitude),
Cesium.Math.toDegrees(carto.longitude),
];
const viewer = {
camera: {
positionCartographic: Cesium.Cartographic.fromDegrees(-71, 42.3, 12000),
changed: { addEventListener() {}, removeEventListener() {} },
},
scene: {
sampleHeightSupported: true,
sampleHeight(carto) {
calls.sampleHeight += 1;
thisTurn += 1;
if (thisTurn > mostInATurn) mostInATurn = thisTurn;
busyWait(SAMPLE_COST_MS);
return groundAt(...degrees(carto));
},
globe: {
show: terrainGlobe,
tilesLoaded: true,
getHeight(carto) {
calls.getHeight += 1;
const [lat, lon] = degrees(carto);
return unloaded(lat) ? undefined : groundAt(lat, lon);
},
},
primitives: { add() {}, remove() {} },
},
};
const surface = createTerrainSurface();
const services = {
render: {
governorRequestRender() {
calls.render += 1;
},
},
overlays: {
setOverlayEntries() {},
setOverlaySourceVisible() {},
clearOverlaySource() {},
},
sprites: { registerSpriteCollection() {}, restoreSpriteOrder() {} },
picking: { registerPickOwner() {}, unregisterPickOwner() {} },
};
if (shared) {
services.ground = {
terrainSurface: {
...surface,
height(...args) {
calls.sharedHeight += 1;
return surface.height(...args);
},
},
};
}
const state = createState({ services });
const parts = {};
const context = { state, services, parts, source: {} };
parts.model = createModel(context);
parts.rendering = createRendering(context);
parts.selection = {
_installClickHandler() {},
_clearSelection() {},
_onKeyDown() {},
};
parts.ingestion = { abortAllInFlight() {} };
// Leaves the points alone, so a test of disable() sees what disable() stops.
parts.viewport = {
onCameraChanged() {},
deactivateAllCities() {},
runProximityCheck: async () => {},
};
parts.lifecycle = createLifecycle(context);
parts.lifecycle.methods.init(viewer);
state._pointCollection = {
show: true,
add: (options) => ({ ...options }),
remove() {},
};
const heightOf = (key) =>
Cesium.Cartographic.fromCartesian(
state._stationRenderMap.get(key).point.position,
).height;
return {
state,
parts,
viewer,
calls,
rendering: parts.rendering,
lifecycle: parts.lifecycle.methods,
heightOf,
add(cityId, count) {
const info = parts.model.parseStationInformation(stations(count, cityId));
parts.rendering.ensureCityPoints(cityId, info);
return info;
},
/** Wait until every queued station has its height, or fail. */
async refined() {
const deadline = performance.now() + 20_000;
while (parts.rendering.pendingHeightCount() > 0) {
assert.ok(performance.now() < deadline, 'refinement finished');
await new Promise((resolve) => setTimeout(resolve, 5));
}
},
/** The most sampleHeight calls made without yielding to the loop. */
longestBurst() {
return mostInATurn;
},
done() {
ticking = false;
parts.rendering.stopHeightRefinement();
},
};
}
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
test('a city of stations is drawn at once without sampling heights', (t) => {
const layer = makeLayer();
t.after(layer.done);
const started = performance.now();
layer.add('boston', 300);
const spent = performance.now() - started;
assert.equal(layer.state._stationRenderMap.size, 300, 'every station drawn');
assert.equal(layer.calls.sampleHeight, 0, 'no depth render while drawing');
assert.ok(spent < 100, `drawing took ${spent.toFixed(1)} ms`);
assert.ok(
Math.abs(layer.heightOf('boston:boston0') - POINT_HEIGHT_OFFSET_M) < 1e-3,
'drawn at the fallback height',
);
assert.equal(layer.rendering.pendingHeightCount(), 300);
});
test('heights arrive afterwards in short steps and match a direct sample', async (t) => {
const layer = makeLayer();
t.after(layer.done);
const info = layer.add('boston', 120);
const rendersBefore = layer.calls.render;
await layer.refined();
assert.equal(layer.calls.sampleHeight, 120, 'one sample per station');
// 120 ms of samples in one go is the stall; a step is a few milliseconds.
assert.ok(
layer.longestBurst() <= 10,
`longest burst was ${layer.longestBurst()} samples`,
);
for (const station of info.values()) {
const expected = groundAt(station.lat, station.lon) + POINT_HEIGHT_OFFSET_M;
const actual = layer.heightOf(`boston:${station.stationId}`);
assert.ok(
Math.abs(actual - expected) < 1e-3,
`${station.stationId}: ${actual} vs ${expected}`,
);
}
assert.ok(layer.calls.render > rendersBefore, 'moved points ask for a frame');
});
test('on a terrain globe the shared terrain surface answers, not a depth render', async (t) => {
const layer = makeLayer({ terrainGlobe: true, shared: true });
t.after(layer.done);
const info = layer.add('boston', 200);
assert.equal(layer.calls.getHeight, 0, 'nothing read while drawing');
await layer.refined();
assert.equal(layer.calls.sampleHeight, 0);
assert.equal(layer.calls.sharedHeight, 200, 'asked the shared surface');
assert.equal(layer.calls.getHeight, 200);
for (const station of info.values()) {
const expected = groundAt(station.lat, station.lon) + POINT_HEIGHT_OFFSET_M;
const actual = layer.heightOf(`boston:${station.stationId}`);
assert.ok(Math.abs(actual - expected) < 1e-3, station.stationId);
}
});
test('without a shared surface the layer reads the terrain globe itself', async (t) => {
const layer = makeLayer({ terrainGlobe: true });
t.after(layer.done);
layer.add('boston', 50);
await layer.refined();
assert.equal(layer.calls.sampleHeight, 0);
assert.equal(layer.calls.getHeight, 50);
assert.ok(
Math.abs(
layer.heightOf('boston:boston7') -
(groundAt(42.3007, -71.0993) + POINT_HEIGHT_OFFSET_M),
) < 1e-3,
);
});
test('a station whose terrain tile is not drawn falls back to a depth render', async (t) => {
// Stations 10 and up lie on a tile the globe is not drawing.
const layer = makeLayer({
terrainGlobe: true,
shared: true,
unloaded: (lat) => lat > 42.30095,
});
t.after(layer.done);
layer.add('boston', 20);
await layer.refined();
assert.equal(layer.calls.sampleHeight, 10);
assert.ok(
Math.abs(
layer.heightOf('boston:boston15') -
(groundAt(42.3015, -71.0985) + POINT_HEIGHT_OFFSET_M),
) < 1e-3,
);
});
test('removing a city stops its refinement and leaves the other running', async (t) => {
const layer = makeLayer();
t.after(layer.done);
layer.add('boston', 40);
layer.add('toronto', 40);
layer.rendering.removeCityPoints('boston');
assert.equal(layer.rendering.pendingHeightCount(), 40);
await layer.refined();
assert.equal(layer.calls.sampleHeight, 40, 'only the remaining city');
assert.ok(layer.heightOf('toronto:toronto3') > 50);
layer.add('boston', 40);
layer.rendering.removeCityPoints('boston');
layer.rendering.removeCityPoints('toronto');
assert.equal(layer.rendering.pendingHeightCount(), 0);
assert.equal(layer.rendering.heightRefinementScheduled(), false);
await pause(60);
assert.equal(layer.calls.sampleHeight, 40, 'nothing sampled after removal');
});
test('disabling the layer stops refinement and leaves no timer', async (t) => {
const layer = makeLayer();
t.after(layer.done);
layer.lifecycle.enable(layer.viewer);
layer.add('boston', 200);
await pause(30);
const partWay = layer.calls.sampleHeight;
assert.ok(partWay < 200, 'still refining');
layer.lifecycle.disable(layer.viewer);
assert.equal(layer.rendering.pendingHeightCount(), 0);
assert.equal(layer.rendering.heightRefinementScheduled(), false);
await pause(60);
assert.equal(layer.calls.sampleHeight, partWay, 'no sample after disable');
});
test('a station selected before its height arrives is re-selected at it', async (t) => {
const layer = makeLayer();
t.after(layer.done);
layer.add('boston', 3);
const reselected = [];
layer.state._selectedKey = 'boston:boston1';
// The selection part's select path; here it records the key.
layer.parts.selection._selectStation = (key) => reselected.push(key);
await layer.refined();
assert.deepEqual(reselected, ['boston:boston1']);
});
+3
View File
@@ -44,6 +44,7 @@ export function createLifecycle({
layerState._inFlightInfo = new Map();
layerState._inFlightStatus = new Map();
layerState._stationRenderMap = new Map();
parts.rendering?.stopHeightRefinement?.();
layerState._clickHandler = null;
layerState._selectedKey = null;
layerState._selectedEntity = null;
@@ -134,6 +135,7 @@ export function createLifecycle({
parts.ingestion.abortAllInFlight();
parts.viewport.deactivateAllCities();
parts.rendering?.stopHeightRefinement?.();
layerState._cityRuntime.clear();
layerState._pointCollection.show = false;
layerState._count = 0;
@@ -165,6 +167,7 @@ export function createLifecycle({
layerState._cameraChangedAttached = false;
}
parts.ingestion.abortAllInFlight();
parts.rendering?.stopHeightRefinement?.();
if (layerState._pointCollection) {
viewer.scene.primitives.remove(layerState._pointCollection);
layerState._pointCollection = null;
+11
View File
@@ -50,6 +50,17 @@ export const DEFAULT_CAPACITY = 15;
export const POINT_HEIGHT_OFFSET_M = 2.0;
/**
* Milliseconds one step may spend finding station ground heights. A height
* the terrain globe cannot give is a depth render of a millisecond or more.
*/
export const HEIGHT_REFINE_BUDGET_MS = 4;
/** Milliseconds between those steps, about a frame. */
export const HEIGHT_REFINE_INTERVAL_MS = 16;
/** Hard cap on total rendered station points across all cities. */
export const MAX_TOTAL_POINTS = 8000;
+106 -12
View File
@@ -11,7 +11,13 @@ import {
POINT_HEIGHT_OFFSET_M,
MAX_TOTAL_POINTS,
COLOR_OUTLINE,
HEIGHT_REFINE_BUDGET_MS,
HEIGHT_REFINE_INTERVAL_MS,
} from './policy.js';
import {
createTerrainSurface,
TERRAIN_NEED,
} from '../../services/terrainSurface.js';
export function createRendering({
state: layerState,
@@ -20,6 +26,14 @@ export function createRendering({
source,
}) {
const { governorRequestRender } = services.render;
// The terrain globe's loaded surface: the application's shared reader, or
// the layer's own when it is built without one.
const terrainSurface =
services.ground?.terrainSurface || createTerrainSurface();
/** Stations drawn at the fallback height, waiting for their ground height. */
let heightQueue = [];
let heightTimer = null;
const heightPoint = new Cesium.Cartographic();
/**
* Resolve the effective dock capacity for a station.
@@ -87,8 +101,10 @@ export function createRendering({
}
/**
* Create a Cartesian3 position for a station, terrain-clamped when possible.
* Falls back to a fixed small height offset if terrain sampling is unsupported.
* Create a Cartesian3 position for a station at the fallback height. Its
* ground height is found afterwards (refineHeights): a city's stations are
* drawn at once, and finding hundreds of heights in the same task held the
* main thread for most of a second.
* @param {Object} station - Station info object with lat/lon.
* @returns {Cesium.Cartesian3|null} World position, or null if coordinates are invalid.
*/
@@ -97,18 +113,78 @@ export function createRendering({
const lon = Number(station?.lon);
const lat = Number(station?.lat);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
return Cesium.Cartesian3.fromDegrees(lon, lat, POINT_HEIGHT_OFFSET_M);
}
let height = POINT_HEIGHT_OFFSET_M;
// Sample terrain height so points sit on ground rather than at ellipsoid level
if (layerState._viewer?.scene?.sampleHeightSupported) {
const carto = Cesium.Cartographic.fromDegrees(lon, lat);
const sampled = layerState._viewer.scene.sampleHeight(carto);
if (Number.isFinite(sampled)) {
height = sampled + POINT_HEIGHT_OFFSET_M;
}
/**
* The ground height under a station, or undefined when the scene cannot say.
* A terrain globe answers from its loaded surface, which is cheap. Where it
* cannot (the globe is hidden under photorealistic tiles, or draws no tile
* at the point) the scene renders the depth there, at a millisecond or more
* a point.
*/
function sampleGroundHeight(lat, lon) {
const viewer = layerState._viewer;
const scene = viewer?.scene;
if (!scene) return undefined;
// A station is a marker: its one read is not left for a later window.
// The step's own time budget bounds what a pass spends.
const terrain = terrainSurface.height(
viewer,
lat,
lon,
TERRAIN_NEED.MARKER,
);
if (Number.isFinite(terrain)) return terrain;
if (!scene.sampleHeightSupported) return undefined;
Cesium.Cartographic.fromDegrees(lon, lat, 0, heightPoint);
const sampled = scene.sampleHeight(heightPoint);
return Number.isFinite(sampled) ? sampled : undefined;
}
/**
* Move queued stations onto the ground for a few milliseconds, then leave
* the rest to the next step. A station the scene has no height for keeps
* the fallback height, as it did when heights were read while drawing.
*/
function refineHeights() {
heightTimer = null;
const started = performance.now();
let moved = false;
while (
heightQueue.length > 0 &&
performance.now() - started < HEIGHT_REFINE_BUDGET_MS
) {
const { record, lat, lon } = heightQueue.pop();
const ground = sampleGroundHeight(lat, lon);
if (ground === undefined) continue;
record.point.position = Cesium.Cartesian3.fromDegrees(
lon,
lat,
ground + POINT_HEIGHT_OFFSET_M,
);
moved = true;
// The highlight and card of a selected station copy its position.
if (layerState._selectedKey === record.key)
parts.selection._selectStation(record.key);
}
if (moved) governorRequestRender('bikeshare-heights');
scheduleHeightRefinement();
}
return Cesium.Cartesian3.fromDegrees(lon, lat, height);
function scheduleHeightRefinement() {
if (heightTimer !== null || heightQueue.length === 0) return;
heightTimer = setTimeout(refineHeights, HEIGHT_REFINE_INTERVAL_MS);
}
/** Drop every queued station and the pending step. */
function stopHeightRefinement() {
heightQueue = [];
clearTimeout(heightTimer);
heightTimer = null;
}
/**
@@ -178,7 +254,7 @@ export function createRendering({
id: key,
});
layerState._stationRenderMap.set(key, {
const record = {
key,
cityId,
stationId: station.stationId,
@@ -191,12 +267,19 @@ export function createRendering({
isInstalled: station.isInstalled,
isRenting: station.isRenting,
isReturning: station.isReturning,
};
layerState._stationRenderMap.set(key, record);
heightQueue.push({
record,
lat: Number(station.lat),
lon: Number(station.lon),
});
runtime.stationKeys.add(key);
}
layerState._count = layerState._stationRenderMap.size;
scheduleHeightRefinement();
}
/**
@@ -224,6 +307,12 @@ export function createRendering({
layerState._stationRenderMap.delete(key);
}
// Its stations no longer wait for a height.
heightQueue = heightQueue.filter(
({ record }) => layerState._stationRenderMap.get(record.key) === record,
);
if (heightQueue.length === 0) stopHeightRefinement();
layerState._cityRuntime.delete(cityId);
layerState._count = layerState._stationRenderMap.size;
}
@@ -280,5 +369,10 @@ export function createRendering({
ensureCityPoints,
removeCityPoints,
applyStatusToPoints,
stopHeightRefinement,
/** Stations still waiting for their ground height. */
pendingHeightCount: () => heightQueue.length,
/** Whether a refinement step is waiting on a timer. */
heightRefinementScheduled: () => heightTimer !== null,
};
}
+7 -51
View File
@@ -260,21 +260,6 @@ export function createGeometry({ state: layerState, services, parts, source }) {
return Math.max(PROBE_MIN_RANGE_M, hitDistance - PROBE_CLEARANCE_M);
}
/**
* Distance along a pitched-down view axis to flat ground at the mount's own
* ground height, or null for an axis at or above the horizon.
* @param {number} mountHeightM Mount height above its ground.
* @param {number} pitchDeg Axis pitch (negative looks down).
* @returns {number|null}
*/
function flatGroundAxisDistance(mountHeightM, pitchDeg) {
const height = Number(mountHeightM);
const pitch = Number(pitchDeg);
if (!Number.isFinite(height) || height <= 0 || !(pitch < 0)) return null;
return height / Math.sin(Cesium.Math.toRadians(Math.min(89, -pitch)));
}
/**
* Converts a computeFrustumGeometry result into the Cartesian3 positions the
* entities consume. Fresh objects per call (geometry updates are rare —
@@ -631,14 +616,10 @@ export function createGeometry({ state: layerState, services, parts, source }) {
* range, clamp the plane's effective range just short of the first hit so the
* "big and dramatic" true end cap never clips into downtown buildings.
*
* On a globe map it also picks the globe's terrain along the same axis
* (flat ground at the mount's ground when terrain is missed or met beyond
* the range) and clamps at the nearer end. These are the ONLY raycasts in the
* whole CCTV subsystem — once per activation, never per-frame (the
* zero-raycast invariant applies to steady state). The per-camera range
* slider overrides the clamp: a user-set rangeScale skips the probe
* entirely. A miss on the photoreal stack or a probe failure keeps the
* unclamped range.
* This is the ONLY raycast in the whole CCTV subsystem — once per activation,
* never per-frame (the zero-raycast invariant applies to steady state). The
* per-camera range slider overrides the clamp: a user-set rangeScale skips the
* probe entirely. Probe failure/miss keeps the unclamped range.
* @param {Object} record - Camera record being activated.
*/
@@ -665,33 +646,9 @@ export function createGeometry({ state: layerState, services, parts, source }) {
for (const runtime of layerState._projectionEntities) {
if (runtime?.planeEntity) exclude.push(runtime.planeEntity);
}
const ray = new Cesium.Ray(mountPos, dir);
// pickFromRay hits only primitives (the photoreal tiles), never the
// globe: on a globe map the ray runs on through the Earth and can meet
// a primitive thousands of kilometres away, which blocks nothing. Such
// a hit or none left the plane at the full nominal range, about a
// kilometre wide and hundreds of metres up. A hit at or beyond the range
// counts as none. On a globe map the axis also ends on the globe's
// terrain, the nearer of that and any primitive hit (a primitive behind
// a hill the axis has already entered blocks nothing). Terrain still
// loading at a coarse level can be missed or met beyond the range; then
// flat ground at the mount's own ground stands in for it.
const inRange = (distance) =>
Number.isFinite(distance) && distance > 0 && distance < camera.rangeM;
const hit = scene.pickFromRay(ray, exclude)?.position;
const hitDist = hit ? Cesium.Cartesian3.distance(mountPos, hit) : NaN;
const distances = inRange(hitDist) ? [hitDist] : [];
if (scene.globe?.show) {
const terrain = scene.globe.pick(ray, scene);
const terrainDist = terrain
? Cesium.Cartesian3.distance(mountPos, terrain)
: NaN;
const ground = inRange(terrainDist)
? terrainDist
: flatGroundAxisDistance(camera.mountHeightM, camera.pitchDeg);
if (Number.isFinite(ground) && ground > 0) distances.push(ground);
}
const dist = distances.length ? Math.min(...distances) : null;
const hit = scene.pickFromRay(new Cesium.Ray(mountPos, dir), exclude);
if (!hit?.position) return;
const dist = Cesium.Cartesian3.distance(mountPos, hit.position);
record.probeClampRangeM = activationProbeClampRange(camera.rangeM, dist);
} catch {
// probe failure → keep the unclamped range
@@ -874,7 +831,6 @@ export function createGeometry({ state: layerState, services, parts, source }) {
hasShippedFootprint,
footprintPose,
activationProbeClampRange,
flatGroundAxisDistance,
frustumCartesians,
frustumFrameEcef,
applyFrustumGeometry,
-1
View File
@@ -73,7 +73,6 @@ export function createCctvLayer({ services, source }) {
deriveCalBadge: parts.calibration.deriveCalBadge,
computeFrustumGeometry: parts.geometry.computeFrustumGeometry,
activationProbeClampRange: parts.geometry.activationProbeClampRange,
flatGroundAxisDistance: parts.geometry.flatGroundAxisDistance,
frameSignatureFromPixels: parts.frames.frameSignatureFromPixels,
_createCctvProjectionPlaneForTest:
parts.testing._createCctvProjectionPlaneForTest,
-3
View File
@@ -79,9 +79,6 @@ export function createSelection({
record,
)
) {
// Picking the camera a settled move re-opened is still a choice: later
// settled moves must leave it alone.
layerState._openingCameraProvisional = false;
// Browser-only feeds have a separate player; a reselect retries it
// without re-running activation or touching the projection geometry.
if (parts.model.isBrowserOverlayCamera(record.camera)) {
+4 -1
View File
@@ -32,7 +32,10 @@ test('a FIRMS snapshot that ends at a set time names it instead of LIVE', (conte
assert.equal(methods.getStats().at, at);
assert.equal(methods.getStats().source, 'NASA FIRMS', 'the row drops LIVE');
setLocale('de');
assert.equal(methods.getStats().loadingLabel, '24 h bis 21.10. 15:00 UTC');
assert.equal(
methods.getStats().loadingLabel,
'24 Std. bis 21.10. 15:00 UTC',
);
layerState._at = null;
setLocale('en');
assert.equal(methods.getStats().loadingLabel, 'LIVE · updated 2m ago');
+25 -3
View File
@@ -1,6 +1,10 @@
import * as Cesium from 'cesium';
import { applyDisplayCourse, updatePlayback } from '../transit/movement.js';
import { HEIGHT_SAMPLE_MAX_ALTITUDE_M } from '../transit/policy.js';
import {
TERRAIN_NEED,
createTerrainSurface,
} from '../../services/terrainSurface.js';
import {
COLD_CELL_FLUSH_MS,
COLD_CELL_RETRY_MS,
@@ -28,6 +32,11 @@ import {
import { placeOnPath } from './pathBinding.js';
const SPRITE_PX = 64;
/**
* Terrain reads per degree for a train's height: about 3.7 m apart. At 11 m
* a train on fine tiles stood up to a couple of metres off the surface.
*/
const TERRAIN_GRID = 3e4;
/** Billboard tints; the setter copies, so sharing one instance is safe. */
const FULL_TINT = Cesium.Color.WHITE;
const DIM_TINT = new Cesium.Color(1, 1, 1, UNSELECTED_TRAIN_ALPHA);
@@ -101,8 +110,11 @@ export function createRendering({ state, services, parts }) {
neighborFloorM = null,
coarseFloorCoord = null,
} = services.ground;
// The terrain globe's loaded surface: the application's shared reader, or
// the layer's own when it is built without one.
const terrainSurface =
services.ground.terrainSurface || createTerrainSurface();
const sprites = new Map();
const terrainPoint = new Cesium.Cartographic();
const scratchPosition = new Cesium.Cartesian3();
/** Scratch for the on-line position; place() runs every frame. */
const pathPoint = { lat: NaN, lon: NaN, bearingDeg: NaN };
@@ -379,8 +391,18 @@ export function createRendering({ state, services, parts }) {
function floorHeight(lat, lon, entry = null, stop = false) {
const globe = state._viewer?.scene?.globe;
if (globe?.show === true) {
Cesium.Cartographic.fromDegrees(lon, lat, 0, terrainPoint);
const rendered = globe.getHeight?.(terrainPoint);
// A moving train is asked about every frame, at a new point each
// time. On a ~4 m grid it asks the terrain a few times a second
// instead, and a standing train not at all; the drawn height is
// smoothed along the track anyway (displayHeightFor). Read as a
// marker, never rationed: a train that starts on a fallback floor only
// creeps to the terrain at a railway's grade.
const rendered = terrainSurface.height(
state._viewer,
Math.round(lat * TERRAIN_GRID) / TERRAIN_GRID,
Math.round(lon * TERRAIN_GRID) / TERRAIN_GRID,
TERRAIN_NEED.MARKER,
);
if (Number.isFinite(rendered))
return answer(rendered + GROUND_FLOOR_LIFT_M, 3);
}
+50 -16
View File
@@ -7,6 +7,10 @@ import {
HEIGHT_SAMPLE_MAX_ALTITUDE_M,
withinViewBounds,
} from './policy.js';
import {
TERRAIN_NEED,
createTerrainSurface,
} from '../../services/terrainSurface.js';
/** Surface preparation off the render path, shared by markers and history. */
export function createHeight({ state, services, parts }) {
@@ -47,35 +51,44 @@ export function createHeight({ state, services, parts }) {
return cameraAltitude() <= HEIGHT_SAMPLE_MAX_ALTITUDE_M;
}
// Loaded-terrain heights come from the application's shared reader, which
// keeps them per point and rations the reads (services/terrainSurface.js).
// A layer built without one keeps its own.
const sharedSurface = services.ground.terrainSurface;
const terrainSurface = sharedSurface || createTerrainSurface();
/**
* The floor under a vehicle, and the best fallback if its own cell is cold.
* @param {number} lat
* @param {number} lon
* @param {?object} [entry] The vehicle, for a fixture floor.
* @param {number} [need] How much the answer matters; see TERRAIN_NEED.
* @returns {{cell: string, height: number|null, prior: number|null}}
*/
const terrainPoint = new Cesium.Cartographic();
function requestHeight(lat, lon, entry = null) {
function requestHeight(lat, lon, entry = null, need = TERRAIN_NEED.PATH) {
const coarse = coarseFloorCoord(lat, lon);
const cell = `${coarse.lat},${coarse.lon}`;
const fixtureFloor =
entry?.qaFloorM ?? state._qaFixtureFloors?.get(entry?.key);
if (Number.isFinite(fixtureFloor))
return { cell, height: fixtureFloor, prior: null };
// On a terrain globe, use the loaded surface under the marker. This is
// synchronous cache work and does not launch a terrain request.
// On a terrain globe, use the loaded surface under the marker. This reads
// tiles already loaded and does not launch a terrain request.
const globe = state._viewer?.scene?.globe;
if (globe?.show === true) {
Cesium.Cartographic.fromDegrees(lon, lat, 0, terrainPoint);
const rendered = globe.getHeight?.(terrainPoint);
const flat =
if (
state._viewer?.terrainProvider instanceof
Cesium.EllipsoidTerrainProvider;
if (Number.isFinite(rendered) || flat)
return {
cell,
height: (flat ? 0 : rendered) + GROUND_FLOOR_LIFT_M,
prior: null,
};
Cesium.EllipsoidTerrainProvider
) {
// Nothing is asked of the terrain while the globe is flat: show the
// reader the flat provider, so the heights kept from the terrain
// before it do not come back with the terrain after it.
terrainSurface.observe(state._viewer);
return { cell, height: GROUND_FLOOR_LIFT_M, prior: null };
}
const rendered = terrainSurface.height(state._viewer, lat, lon, need);
if (Number.isFinite(rendered))
return { cell, height: rendered + GROUND_FLOOR_LIFT_M, prior: null };
}
const floor = cachedGroundFloor(lat, lon);
if (Number.isFinite(floor)) {
@@ -107,6 +120,7 @@ export function createHeight({ state, services, parts }) {
function rereadFloors() {
state._floorTimer = null;
if (!state._enabled) return;
const waitingBefore = terrainSurface.waitingCount();
const onGround = nearGround(),
candidates = [];
const entries = [...state._vehicles.values()].filter(
@@ -120,6 +134,13 @@ export function createHeight({ state, services, parts }) {
Number(b.key === state._selectedKey) -
Number(a.key === state._selectedKey),
);
// Every vehicle's own position before any trail: the first trails of a
// pass must not spend the budget the last markers are waiting on.
for (const entry of entries) {
const newest = entry.fixes.at(-1);
if (newest)
requestHeight(newest.lat, newest.lon, entry, TERRAIN_NEED.MARKER);
}
for (const entry of entries) {
const cells = new Map();
const collect = (point, answer) => {
@@ -141,7 +162,12 @@ export function createHeight({ state, services, parts }) {
};
parts.trails.prepareEntry(entry, collect);
const newest = entry.fixes.at(-1);
const answer = requestHeight(newest.lat, newest.lon, entry);
const answer = requestHeight(
newest.lat,
newest.lon,
entry,
TERRAIN_NEED.MARKER,
);
entry.heightCell = answer.cell;
// The selected vehicle's path also offers its history, newest segment
// first; the vehicle's own cell is read where its newest fix is.
@@ -227,7 +253,11 @@ export function createHeight({ state, services, parts }) {
for (const entry of entries) parts.trails.prepareEntry(entry);
parts.rendering.requestVisibility();
governorRequestRender('transit-floor');
if (state._floorAttempts++ < FLOOR_REREAD_ATTEMPTS)
// A pass that ran out of terrain budget before reaching every point
// comes back for the rest. Each pass reads its budget, and points stop
// falling due once the terrain settles, so this ends.
const waiting = terrainSurface.waitingCount() - waitingBefore;
if (state._floorAttempts++ < FLOOR_REREAD_ATTEMPTS || waiting > 0)
state._floorTimer = setTimeout(rereadFloors, FLOOR_REREAD_MS);
}
@@ -258,6 +288,8 @@ export function createHeight({ state, services, parts }) {
}
state._floorAttempts = 0;
state._heightDirty.clear();
// The shared reader's heights belong to every layer still showing.
if (!sharedSurface) terrainSurface.clear();
}
return {
@@ -267,5 +299,7 @@ export function createHeight({ state, services, parts }) {
anchorFloors,
rereadFloors,
clear,
/** Terrain read counters since the layer was created, for checks. */
terrainStats: () => terrainSurface.stats(),
};
}
+389
View File
@@ -0,0 +1,389 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { createHeight } from './height.js';
import { FLOOR_REREAD_ATTEMPTS, FLOOR_REREAD_MS } from './policy.js';
import {
TERRAIN_HEIGHT_CACHE_MAX,
TERRAIN_HEIGHT_FRESH_MS,
TERRAIN_NEED,
TERRAIN_READ_BUDGET_MS,
TERRAIN_READ_WINDOW_MS,
TERRAIN_REFRESH_BUDGET_MS,
TERRAIN_SETTLE_MS,
} from '../../services/terrainSurface.js';
const LIFT = 1.5;
/**
* A height part over a terrain globe whose every read costs `costMs` of a
* clock the test owns. `surface(lat, lon)` is the terrain; `reads` lists the
* points actually read.
*/
function terrain(t, { costMs = 0, surface = () => 100, provider = {} } = {}) {
const clock = { now: 1_000_000 };
t.mock.method(performance, 'now', () => clock.now);
const reads = [];
const world = { surface, costMs };
const globe = {
show: true,
getHeight(point) {
const lat = Cesium.Math.toDegrees(point.latitude);
const lon = Cesium.Math.toDegrees(point.longitude);
reads.push(`${lat.toFixed(5)},${lon.toFixed(5)}`);
clock.now += world.costMs;
return world.surface(lat, lon);
},
};
const state = {
_enabled: true,
_selectedKey: null,
_vehicles: new Map(),
_visible: new Set(),
_floorCursor: 0,
_floorAttempts: 0,
_floorTimer: null,
_heightDirty: new Set(),
_viewer: {
camera: { positionCartographic: { height: 500 } },
scene: { globe },
terrainProvider: provider,
},
};
const parts = {
trails: { prepareEntry() {} },
rendering: { requestVisibility() {} },
};
const height = createHeight({
state,
services: {
render: { governorRequestRender() {} },
ground: {
coarseFloorCoord: (lat, lon) => ({
lat: Number(lat.toFixed(3)),
lon: Number(lon.toFixed(3)),
}),
cachedGroundFloor: () => null,
neighborFloorM: () => null,
warmGroundFloor() {},
GROUND_FLOOR_LIFT_M: LIFT,
},
},
parts,
});
t.after(() => height.clear());
const nextWindow = () => {
clock.now += TERRAIN_READ_WINDOW_MS;
};
return { height, state, parts, globe, world, clock, reads, nextWindow };
}
test('a point is read from the terrain once and reused until its reuse time passes', (t) => {
const { height, reads, clock, world } = terrain(t);
const first = height.requestHeight(42.36, -71.06);
assert.deepEqual(first, { cell: '42.36,-71.06', height: 100 + LIFT, prior: null });
for (let i = 0; i < 500; i++) height.requestHeight(42.36, -71.06);
assert.equal(reads.length, 1, '501 requests, one terrain read');
// A point a metre away is its own point; the same cell does not stand in
// for it while the budget allows a read.
world.surface = (lat) => (lat > 42.36 ? 140 : 100);
assert.equal(height.requestHeight(42.36002, -71.06).height, 140 + LIFT);
assert.equal(reads.length, 2);
// The surface under the first point changed (the globe drew a finer tile).
world.surface = () => 120;
clock.now += TERRAIN_HEIGHT_FRESH_MS - 1;
assert.equal(height.requestHeight(42.36, -71.06).height, 100 + LIFT);
clock.now += 1;
assert.equal(height.requestHeight(42.36, -71.06).height, 120 + LIFT);
assert.equal(reads.length, 3);
assert.deepEqual(height.terrainStats(), {
reads: 3,
reused: 501,
deferred: 0,
borrowed: 0,
held: 2,
});
});
test('terrain that is not loaded yet is not remembered as an answer', (t) => {
const { height, reads, world } = terrain(t, { surface: () => undefined });
assert.deepEqual(height.requestHeight(42.36, -71.06), {
cell: '42.36,-71.06',
height: null,
prior: null,
});
assert.equal(height.requestHeight(42.36, -71.06).height, null);
assert.equal(reads.length, 2, 'each request asks again');
world.surface = () => 12;
assert.equal(height.requestHeight(42.36, -71.06).height, 12 + LIFT);
assert.equal(height.terrainStats().held, 1);
});
test('a tile that leaves the globe keeps the height it gave', (t) => {
const { height, clock, world } = terrain(t, { surface: () => 30 });
assert.equal(height.requestHeight(42.36, -71.06).height, 30 + LIFT);
world.surface = () => undefined;
clock.now += TERRAIN_HEIGHT_FRESH_MS * 20;
assert.equal(height.requestHeight(42.36, -71.06).height, 30 + LIFT);
});
test('new points stop being read once a window has spent its budget', (t) => {
const costMs = 5;
const { height, reads, nextWindow } = terrain(t, {
costMs,
surface: (lat, lon) => Math.round(lon * -1000),
});
const affordable = Math.ceil(TERRAIN_READ_BUDGET_MS / costMs);
// One point per floor cell (0.001° apart), more than the window affords.
const answers = [];
for (let i = 0; i < affordable + 3; i++)
answers.push(height.requestHeight(42.36, -71.06 - i * 0.001).height);
assert.equal(reads.length, affordable);
assert.deepEqual(
answers.slice(affordable),
[null, null, null],
'a point in a cell never read waits',
);
assert.ok(reads.length * costMs < TERRAIN_READ_BUDGET_MS + costMs, 'the overrun is one read at most');
// A new point in a cell that was read borrows that cell's height.
const borrowed = height.requestHeight(42.36004, -71.06);
assert.equal(borrowed.height, 71060 + LIFT);
assert.equal(reads.length, affordable, 'borrowing reads nothing');
assert.equal(height.terrainStats().borrowed, 1);
// The next window reads the points that waited, and the borrower its own.
nextWindow();
for (let i = affordable; i < affordable + 3; i++)
assert.equal(
height.requestHeight(42.36, -71.06 - i * 0.001).height,
Math.round((-71.06 - i * 0.001) * -1000) + LIFT,
);
assert.equal(reads.length, affordable + 3);
});
test("a vehicle's own position is read even when the window is spent", (t) => {
const { height, reads } = terrain(t, { costMs: TERRAIN_READ_BUDGET_MS });
height.requestHeight(42.36, -71.06);
assert.equal(height.requestHeight(42.37, -71.07).height, null, 'a trail point waits');
assert.equal(height.requestHeight(42.38, -71.08, null, TERRAIN_NEED.MARKER).height, 100 + LIFT);
assert.equal(reads.length, 2);
});
test('undrawn trail history leaves most of the budget to the paths markers are drawn along', (t) => {
const costMs = 1;
const { height, reads } = terrain(t, { costMs });
// New points, one per floor cell: history first, as a pass offers them.
let lon = -71;
const ask = (need) => height.requestHeight(42, (lon -= 0.001), null, need).height;
const history = Array.from({ length: 30 }, () => ask(TERRAIN_NEED.BACKGROUND));
const read = history.filter((h) => h !== null).length;
assert.equal(read, TERRAIN_READ_BUDGET_MS / 4, 'history stops at a quarter of the budget');
const path = Array.from({ length: 30 }, () => ask(TERRAIN_NEED.PATH));
assert.equal(path.filter((h) => h !== null).length, TERRAIN_READ_BUDGET_MS - read, 'the path takes the rest');
assert.equal(ask(TERRAIN_NEED.BACKGROUND), null);
assert.equal(ask(TERRAIN_NEED.MARKER), 100 + LIFT, 'a marker is still read');
assert.equal(reads.length, TERRAIN_READ_BUDGET_MS + 1);
});
test('held points keep their last height while re-reads are over budget, and the longest-held are re-read first so none starves', (t) => {
const costMs = TERRAIN_REFRESH_BUDGET_MS / 2;
const { height, reads, clock, world, nextWindow } = terrain(t, { surface: () => 50 });
const points = Array.from({ length: 40 }, (_, i) => [42.36, -71.06 - i * 0.001]);
for (const [lat, lon] of points) height.requestHeight(lat, lon);
assert.equal(reads.length, 40);
world.costMs = costMs;
world.surface = () => 80;
clock.now += TERRAIN_HEIGHT_FRESH_MS;
reads.length = 0;
const pass = () => points.map(([lat, lon]) => height.requestHeight(lat, lon).height);
const first = pass();
const perWindow = Math.ceil(TERRAIN_REFRESH_BUDGET_MS / costMs);
assert.equal(reads.length, perWindow, 'one window re-reads its budget and no more');
assert.deepEqual(first.slice(0, perWindow), Array(perWindow).fill(80 + LIFT));
assert.deepEqual(first.slice(perWindow), Array(40 - perWindow).fill(50 + LIFT), 'the rest keep their last height');
// Pass after pass, a window apart: the budget moves down the list instead
// of returning to the first points, whose reuse time runs out again long
// before the last ones have had a turn (40 points, ${perWindow} a window).
const turns = new Map();
for (const point of reads) turns.set(point, 1);
const cycle = Math.ceil(40 / perWindow);
for (let passes = 1; passes < cycle; passes++) {
nextWindow();
reads.length = 0;
pass();
for (const point of reads) turns.set(point, (turns.get(point) || 0) + 1);
}
assert.equal(turns.size, 40, 'every point was re-read in one cycle');
assert.ok([...turns.values()].every((count) => count === 1), 'and none twice before the others once');
assert.ok(cycle * TERRAIN_READ_WINDOW_MS > TERRAIN_HEIGHT_FRESH_MS, 'the cycle is longer than the reuse time, so the first points were stale again');
assert.deepEqual(pass().slice(-1), [80 + LIFT]);
});
test('once the camera and the tiles have settled a height is kept until either changes', (t) => {
const { height, state, globe, reads, clock, world } = terrain(t, { surface: () => 10 });
const read = () => height.requestHeight(42.36, -71.06).height;
assert.equal(read(), 10 + LIFT);
// Still settling: the point falls due and is read again.
world.surface = () => 20;
clock.now += TERRAIN_SETTLE_MS;
assert.equal(read(), 20 + LIFT);
assert.equal(reads.length, 2);
// Read after the settle time: kept, however long the view stays put.
world.surface = () => 99;
for (let i = 0; i < 50; i++) {
clock.now += 60_000;
assert.equal(read(), 20 + LIFT);
}
assert.equal(reads.length, 2);
// The camera moves: the globe may draw the point from a finer tile.
state._viewer.camera.positionCartographic = { longitude: 0, latitude: 0, height: 250 };
clock.now += TERRAIN_READ_WINDOW_MS;
assert.equal(read(), 99 + LIFT);
assert.equal(reads.length, 3);
// Settled again, then a tile starts loading with the camera still.
world.surface = () => 101;
clock.now += TERRAIN_SETTLE_MS;
assert.equal(read(), 101 + LIFT);
clock.now += 60_000;
assert.equal(read(), 101 + LIFT);
assert.equal(reads.length, 4);
world.surface = () => 105;
globe.tilesLoaded = false;
clock.now += TERRAIN_READ_WINDOW_MS;
assert.equal(read(), 105 + LIFT);
assert.equal(reads.length, 5);
});
test('another terrain provider or globe is read afresh, however settled the old heights were', (t) => {
const { height, state, globe, world, clock, reads } = terrain(t);
const viewer = state._viewer;
const at = () => height.requestHeight(42.36, -71.06).height;
// Read once the camera and tiles have long settled: kept indefinitely.
height.requestHeight(10, 10);
clock.now += TERRAIN_SETTLE_MS * 2;
reads.length = 0;
assert.equal(at(), 100 + LIFT);
clock.now += TERRAIN_SETTLE_MS * 10;
world.surface = () => 250;
assert.equal(at(), 100 + LIFT, 'same terrain: the kept height stands');
assert.equal(reads.length, 1);
viewer.terrainProvider = {};
assert.equal(at(), 250 + LIFT, 'a new provider is a new surface');
assert.equal(reads.length, 2);
world.surface = () => 400;
viewer.scene.globe = { show: true, getHeight: globe.getHeight };
assert.equal(at(), 400 + LIFT, 'and so is a new globe');
assert.equal(reads.length, 3);
// Terrain → flat → the same terrain provider: nothing asks the terrain
// while the globe is flat, yet the old heights must not come back.
const real = viewer.terrainProvider;
viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider();
assert.equal(at(), LIFT);
world.surface = () => 30;
viewer.terrainProvider = real;
assert.equal(at(), 30 + LIFT);
assert.equal(reads.length, 4);
});
test('a flat globe answers without reading terrain', (t) => {
const { height, reads } = terrain(t, {
provider: new Cesium.EllipsoidTerrainProvider(),
});
assert.deepEqual(height.requestHeight(42.36, -71.06), {
cell: '42.36,-71.06',
height: LIFT,
prior: null,
});
assert.equal(reads.length, 0);
});
test('a hidden globe reads no terrain and clear() forgets what was read', (t) => {
const { height, reads, globe } = terrain(t);
height.requestHeight(42.36, -71.06);
height.clear();
assert.equal(height.terrainStats().held, 0);
height.requestHeight(42.36, -71.06);
assert.equal(reads.length, 2, 'a cleared layer reads again');
globe.show = false;
assert.equal(height.requestHeight(42.5, -71.5).height, null);
assert.equal(reads.length, 2);
});
test('the points kept stay bounded', (t) => {
const { height } = terrain(t);
const total = TERRAIN_HEIGHT_CACHE_MAX + 5_000;
for (let i = 0; i < total; i++)
height.requestHeight(10 + Math.floor(i / 1000) * 0.001, 20 + (i % 1000) * 0.0001);
const { held, reads } = height.terrainStats();
assert.equal(reads, total);
assert.ok(held <= TERRAIN_HEIGHT_CACHE_MAX, `${held} held`);
assert.ok(held >= TERRAIN_HEIGHT_CACHE_MAX * 0.7, `${held} held`);
});
test('a fleet rereading every trail fix reads each point once, not once per request', (t) => {
const { height, state, parts, reads } = terrain(t);
t.mock.timers.enable({ apis: ['setTimeout'] });
// 600 vehicles with 40 fixes each, 25 m apart along a street.
const fleet = Array.from({ length: 600 }, (_, v) => ({
key: `bus-${v}`,
fixes: Array.from({ length: 40 }, (_, i) => ({
lat: 42 + v * 0.002,
lon: -71 + i * 0.0003,
})),
}));
for (const entry of fleet) {
state._vehicles.set(entry.key, entry);
state._visible.add(entry);
}
let requests = 0;
parts.trails.prepareEntry = (entry, collect) => {
for (const fix of entry.fixes) {
requests++;
const answer = height.requestHeight(fix.lat, fix.lon, entry);
if (collect) collect(fix, answer);
}
};
height.anchorFloors();
for (let i = 0; i < FLOOR_REREAD_ATTEMPTS; i++) t.mock.timers.tick(FLOOR_REREAD_MS);
assert.equal(requests, 600 * 40 * 2 * (FLOOR_REREAD_ATTEMPTS + 1));
assert.equal(new Set(reads).size, 600 * 40);
assert.equal(reads.length, 600 * 40, `${requests} requests, ${reads.length} terrain reads`);
assert.ok(fleet.every((entry) => entry.heightM === 100 + LIFT && entry.heightResolved));
});
test('a reread cycle outlasts its attempts while trail points still wait for a read, then stops', (t) => {
const costMs = 4;
const { height, state, parts, reads, clock } = terrain(t, { costMs });
t.mock.timers.enable({ apis: ['setTimeout'] });
const entry = {
key: 'bus',
// One fix per floor cell, so nothing can be borrowed.
fixes: Array.from({ length: 60 }, (_, i) => ({ lat: 42, lon: -71 + i * 0.001 })),
};
state._vehicles.set(entry.key, entry);
state._visible.add(entry);
parts.trails.prepareEntry = (e) => {
for (const fix of e.fixes) height.requestHeight(fix.lat, fix.lon, e);
};
height.anchorFloors();
let passes = 1;
while (state._floorTimer && passes < 200) {
clock.now += FLOOR_REREAD_MS;
t.mock.timers.tick(FLOOR_REREAD_MS);
passes++;
}
assert.equal(new Set(reads).size, 60, 'every fix was read in the end');
assert.ok(passes > FLOOR_REREAD_ATTEMPTS + 1, `${passes} passes: the cycle kept going for the unread fixes`);
assert.ok(passes < 120, `${passes} passes: and stopped once none waited`);
assert.equal(state._floorTimer, null);
});
+13 -2
View File
@@ -13,6 +13,7 @@ import {
transitVehicleKey,
vehicleReportTimeMs,
} from './policy.js';
import { TERRAIN_NEED } from '../../services/terrainSurface.js';
import {
transitModeFor,
transitModeResolved,
@@ -272,7 +273,12 @@ export function createIngestion({ state, services, parts, source }) {
// DEM one, or a change of map regime, moves a parked vehicle too. A
// cell still cold takes the lowest neighbour as a prior, as before.
const priorHeightM = entry.heightM;
const height = parts.height.requestHeight(record.lat, record.lon);
const height = parts.height.requestHeight(
record.lat,
record.lon,
null,
TERRAIN_NEED.MARKER,
);
if (height.height !== null) {
entry.heightCell = height.cell;
entry.heightM = height.height;
@@ -309,7 +315,12 @@ export function createIngestion({ state, services, parts, source }) {
}
continue;
}
const height = parts.height.requestHeight(record.lat, record.lon);
const height = parts.height.requestHeight(
record.lat,
record.lon,
null,
TERRAIN_NEED.MARKER,
);
// Keep the local prior separate from resolved historical floors.
// A neighbouring cell is not a guaranteed lower bound.
const prior = height.height === null ? height.prior : height.height;
-1
View File
@@ -77,7 +77,6 @@ export const FLOOR_WARM_PER_POLL = 300;
export const FLOOR_REREAD_MS = 900;
/** Re-reads before a poll's cycle gives up and waits for the next poll. */
export const FLOOR_REREAD_ATTEMPTS = 6;
// --- Rendering ---
/** On-screen glyph size in CSS px at the near end of the distance ramp. */
export const MARKER_PIXEL_SIZE = 20;
+20 -8
View File
@@ -8,6 +8,7 @@ import {
} from '../../data/contactPlayback.js';
import { createContactTrailRenderer } from '../../data/contactTrailRenderer.js';
import { fixDistanceM } from './movement.js';
import { TERRAIN_NEED } from '../../services/terrainSurface.js';
import { transitStyleProfile } from '../../data/transitPresetStyle.js';
const MODE_COLOR = {
@@ -72,8 +73,13 @@ export function createTrails({ state, services, parts, source }) {
b = {};
let vertices = 0,
signature = '';
const floor = (point) => {
const answer = parts.height.requestHeight(point.lat, point.lon, entry);
const floor = (point, need = TERRAIN_NEED.PATH) => {
const answer = parts.height.requestHeight(
point.lat,
point.lon,
entry,
need,
);
if (collect) collect(point, answer);
return answer.height;
};
@@ -99,12 +105,6 @@ export function createTrails({ state, services, parts, source }) {
lon: a.lon + ((b.lon - a.lon) * i) / n,
});
}
for (let i = 0; i < entry.track.count; i++) {
readFix(entry.track, i, a);
const h = floor(a);
if (h !== null) correctHeight(entry.track, a.seq, h);
}
const order = [];
const activeIndex = Math.max(
0,
Math.min(
@@ -112,6 +112,18 @@ export function createTrails({ state, services, parts, source }) {
entry.sample.fromSeq - entry.track.baseSeq,
),
);
for (let i = 0; i < entry.track.count; i++) {
readFix(entry.track, i, a);
// Fixes behind an unselected vehicle are kept but not drawn.
const h = floor(
a,
entry === selected || i >= activeIndex
? TERRAIN_NEED.PATH
: TERRAIN_NEED.BACKGROUND,
);
if (h !== null) correctHeight(entry.track, a.seq, h);
}
const order = [];
for (let i = activeIndex; i + 1 < entry.track.count; i++) order.push(i);
if (entry === selected)
for (let i = activeIndex - 1; i >= 0; i--) order.push(i);
-5
View File
@@ -7,7 +7,6 @@ import { createRendering } from './rendering.js';
import { createSelection } from './selection.js';
import { createTracking } from './tracking.js';
import { createCards } from './cards.js';
import { createPhotos } from './photos.js';
import { createTesting } from './testing.js';
import { createEvidence } from './evidence.js';
import { createQueries } from './queries.js';
@@ -25,7 +24,6 @@ export function createVesselLayer({ source, services, options = {} } = {}) {
parts.selection = createSelection(context);
parts.tracking = createTracking(context);
parts.cards = createCards(context);
parts.photos = createPhotos(context);
parts.testing = createTesting(context);
parts.evidence = createEvidence(context);
parts.queries = createQueries(context);
@@ -92,9 +90,6 @@ export function createVesselLayer({ source, services, options = {} } = {}) {
buildVesselCard: parts.cards.buildVesselCard,
buildSelectedVesselCard: parts.cards.buildSelectedVesselCard,
cardScreenSeparated: parts.cards.cardScreenSeparated,
/** The vessel's Open Vessel Photos photo, once its lookup answered. */
getVesselPhoto: (id) =>
parts.photos.vesselPhoto(vesselState.state.records.byId.get(id)),
});
Object.defineProperty(layer, 'testing', { value: parts.testing });
return layer;
-56
View File
@@ -1,56 +0,0 @@
import { MMSI, parseOpenVesselPhoto } from '../../data/openVesselPhoto.js';
/** A failed lookup is asked again after this long; a miss is final. */
const PHOTO_RETRY_MS = 5 * 60 * 1000;
/** Lookups remembered per session, oldest dropped first. */
const PHOTO_CACHE_LIMIT = 500;
/**
* The selected vessel's photo, looked up once per MMSI when it is selected.
* The answer is read with `parseOpenVesselPhoto` whatever shape the source
* returns, so only a checked photo is ever kept. A source without
* `getEnrichment` has no photos.
*/
export function createPhotos({ vesselState }) {
/** MMSI → photo, `null` for a final miss, or `{failedAtMs}`/`{pending}`. */
const answers = new Map();
function remember(mmsi, answer) {
answers.delete(mmsi);
answers.set(mmsi, answer);
if (answers.size > PHOTO_CACHE_LIMIT)
answers.delete(answers.keys().next().value);
}
/** Ask for the record's photo unless it is known, asked or cooling down. */
function requestVesselPhoto(record, now = Date.now()) {
const mmsi = String(record?.mmsi || '');
const source = vesselState._source;
if (!MMSI.test(mmsi) || typeof source?.getEnrichment !== 'function') return;
const known = answers.get(mmsi);
if (known !== undefined) {
if (known?.pending || known === null || known.found) return;
if (now - known.failedAtMs < PHOTO_RETRY_MS) return;
}
remember(mmsi, { pending: true });
Promise.resolve()
.then(() => source.getEnrichment({ kind: 'photo', id: mmsi }))
.then(
(answer) => {
const photo = parseOpenVesselPhoto(answer);
remember(mmsi, photo.found ? photo : null);
},
() => remember(mmsi, { failedAtMs: Date.now() }),
);
}
/** The vessel's checked photo, or null. */
function vesselPhoto(record) {
const answer = answers.get(String(record?.mmsi || ''));
if (!answer?.found) return null;
const { src, width, height, photographer, licence, link } = answer;
return { src, width, height, photographer, licence, link };
}
return { requestVesselPhoto, vesselPhoto };
}
-87
View File
@@ -1,87 +0,0 @@
// src/layers/vessels/photos.test.mjs — a selected vessel's photo is asked for
// once per MMSI; a miss is final and a failure is retried only after a pause.
import test from 'node:test';
import assert from 'node:assert/strict';
import { createPhotos } from './photos.js';
const ANSWER = Object.freeze({
mmsi: '366999709',
photos: [
{
urls: {
mmsi: 'https://openvesselphotos.org/v1/mmsi/366999709/photos/1.jpg',
},
credit: 'Gordon Leggett',
licenceName: 'CC BY 4.0',
width: 1600,
height: 1200,
},
],
});
const settle = () => new Promise((resolve) => setImmediate(resolve));
function fixture(answer) {
const asked = [];
const source = {
getSnapshot() {},
async getEnrichment(query) {
asked.push(query);
return answer(query);
},
};
const photos = createPhotos({ vesselState: { _source: source } });
return { photos, asked };
}
test('a selected vessel is asked once and shows its checked photo', async () => {
const { photos, asked } = fixture(() => ANSWER);
const record = { id: 'v1', mmsi: '366999709' };
photos.requestVesselPhoto(record);
photos.requestVesselPhoto(record);
await settle();
photos.requestVesselPhoto(record);
assert.deepEqual(asked, [{ kind: 'photo', id: '366999709' }]);
assert.deepEqual(photos.vesselPhoto(record), {
src: 'https://openvesselphotos.org/v1/mmsi/366999709/photos/1.jpg?size=800',
width: 800,
height: 600,
photographer: 'Gordon Leggett',
licence: 'CC BY 4.0',
link: 'https://openvesselphotos.org/vessels/mmsi/366999709',
});
});
test('a vessel without a valid MMSI, or a source without lookups, is never asked', async () => {
const { photos, asked } = fixture(() => ANSWER);
photos.requestVesselPhoto({ id: 'v1', mmsi: '' });
photos.requestVesselPhoto({ id: 'v2', mmsi: '12345' });
await settle();
assert.equal(asked.length, 0);
const bare = createPhotos({ vesselState: { _source: { getSnapshot() {} } } });
bare.requestVesselPhoto({ id: 'v3', mmsi: '366999709' });
assert.equal(bare.vesselPhoto({ mmsi: '366999709' }), null);
});
test('a miss is final; a failure is asked again only after the pause', async () => {
let fail = true;
const { photos, asked } = fixture(({ id }) => {
if (id === '111111111') return { mmsi: id, photos: [] };
if (fail) throw new Error('down');
return ANSWER;
});
const missing = { mmsi: '111111111' };
const flaky = { mmsi: '366999709' };
photos.requestVesselPhoto(missing);
photos.requestVesselPhoto(flaky, 0);
await settle();
photos.requestVesselPhoto(missing);
photos.requestVesselPhoto(flaky, Date.now() + 1000);
assert.equal(asked.length, 2);
fail = false;
photos.requestVesselPhoto(flaky, Date.now() + 10 * 60 * 1000);
await settle();
assert.equal(asked.length, 3);
assert.equal(photos.vesselPhoto(missing), null);
assert.equal(photos.vesselPhoto(flaky).photographer, 'Gordon Leggett');
});
-1
View File
@@ -186,7 +186,6 @@ export function createSelection({
// click, not up to VISIBILITY_UPDATE_MS later.
components.rendering.updateVisibility(true);
components.cards.updateSelectedVesselHud(record);
components.photos.requestVesselPhoto(record);
if (registerSelectedContext(record)) {
selectEntityContext(record);
}
+5 -2
View File
@@ -64,9 +64,12 @@ async function standaloneStartup(Cesium, credentials, fetchImpl) {
document: {
getElementById: () => ({ querySelector: () => ({}) }),
},
startPageLocale: () => 'en',
followPageLocale: () => () => {},
resolveLocale: () => 'en',
setLocale() {},
getLocale: () => 'en',
onLocaleChange: () => () => {},
mountApplicationViews() {},
applyDocumentLanguage() {},
applyDocumentTranslations() {},
CITY_POIS: [],
createApplication: (options) => options,
+3
View File
@@ -1,3 +1,4 @@
import { createTerrainSurface } from './terrainSurface.js';
/** Construct an instance-owned groundFloor service with explicit dependencies. */
export function createGroundFloor({ terrain, signal }) {
const { cachedRealEllipsoidalGround, resolveEllipsoidalGround } = terrain;
@@ -698,6 +699,8 @@ export function createGroundFloor({ terrain, signal }) {
{ once: true },
);
return {
// The terrain globe's loaded surface, read once per point for every layer.
terrainSurface: createTerrainSurface(),
GROUND_FLOOR_LIFT_M,
MESH_FLOOR_BELOW_PRIOR_M,
MESH_FLOOR_ABOVE_PRIOR_M,
+259
View File
@@ -0,0 +1,259 @@
import * as Cesium from 'cesium';
/**
* The height of the terrain globe's loaded surface at a point, kept per point
* and read within a time budget.
*
* `globe.getHeight` casts a ray against the mesh of the tile drawn under the
* point: geometry work, not a table lookup, and dearer the finer the tile. A
* layer that asks for every vehicle, trail fix or site on every pass asks the
* same few thousand points over and over, so the answers are kept and the
* reads are rationed. One instance serves every layer of an application, so
* the layers share one budget instead of each spending its own.
*/
/**
* While the camera moves or tiles load, a kept height is reused this long
* before the point is read again.
*/
export const TERRAIN_HEIGHT_FRESH_MS = 2_000;
/**
* A height read once the camera has been still and the globe's tiles loaded
* for this long is kept until either changes: the globe draws a point from
* another tile only after one of them does.
*/
export const TERRAIN_SETTLE_MS = 5_000;
/** Read budgets are counted over windows of this length. */
export const TERRAIN_READ_WINDOW_MS = 200;
/**
* Milliseconds of terrain reads in one window. Past it a point never read
* takes the last height read in its ~111 m floor cell, or waits for a later
* request; a MARKER point is read regardless.
*/
export const TERRAIN_READ_BUDGET_MS = 16;
/**
* The share of that budget re-reads of kept points may use, the longest-kept
* first, leaving the rest for points never read.
*/
export const TERRAIN_REFRESH_BUDGET_MS = 12;
/**
* How much a height matters to what is drawn, lowest first. BACKGROUND is a
* point nobody is looking at (the undrawn trail history of a vehicle) and may
* use a quarter of each budget; PATH is drawn geometry; MARKER is a thing's
* own position, read even when the window's budget is spent, since a marker
* without a height is not drawn.
*/
export const TERRAIN_NEED = Object.freeze({
BACKGROUND: 0,
PATH: 1,
MARKER: 2,
});
/** Points kept; the oldest quarter is dropped past it. */
export const TERRAIN_HEIGHT_CACHE_MAX = 60_000;
/** A number unique to a point on a `scale`-per-degree grid. */
function gridKey(lat, lon, scale) {
return (
(Math.round(lat * scale) + 90 * scale) * (360 * scale + 1) +
Math.round(lon * scale) +
180 * scale
);
}
/**
* @returns {{
* height: (viewer: object, lat: number, lon: number, need?: number) => number|undefined,
* observe: (viewer: object) => void,
* waitingCount: () => number,
* stats: () => {reads: number, reused: number, deferred: number, borrowed: number, held: number},
* clear: () => void,
* }}
*/
export function createTerrainSurface() {
const point = new Cesium.Cartographic();
/** Point key → `{ h, at }`: the height read there and when. */
const heights = new Map();
/** Floor-cell key → the last height read anywhere in that cell. */
const cells = new Map();
/** The globe and terrain provider the kept heights were read from. */
let globeRead = null;
let providerRead = null;
let windowAt = -Infinity;
let readMs = 0;
let refreshMs = 0;
// Per need (background, path): the oldest read among the points this window
// left for later, and the bound the next window re-reads up to.
const deferredAt = [Infinity, Infinity];
const refreshUpTo = [Infinity, Infinity];
let waiting = 0;
/** When the camera last moved or the globe was last loading tiles. */
let changedAt = -Infinity;
let cameraLon = NaN,
cameraLat = NaN,
cameraHeight = NaN;
const counters = { reads: 0, reused: 0, deferred: 0, borrowed: 0 };
function clear() {
heights.clear();
cells.clear();
globeRead = providerRead = null;
windowAt = -Infinity;
readMs = refreshMs = 0;
deferredAt.fill(Infinity);
refreshUpTo.fill(Infinity);
changedAt = -Infinity;
cameraLon = cameraLat = cameraHeight = NaN;
}
/**
* The globe redraws a point from another tile only after the camera moves
* or a tile arrives: note when either was last seen.
*/
function noteChange(viewer, globe, now) {
const camera = viewer.camera?.positionCartographic;
if (
globe.tilesLoaded === false ||
camera?.longitude !== cameraLon ||
camera?.latitude !== cameraLat ||
camera?.height !== cameraHeight
) {
changedAt = now;
cameraLon = camera?.longitude;
cameraLat = camera?.latitude;
cameraHeight = camera?.height;
}
}
/** The viewer's terrain globe, the one whose heights are held, or null. */
function terrainGlobe(viewer) {
const globe = viewer?.scene?.globe;
if (globe?.show !== true || typeof globe.getHeight !== 'function')
return null;
// A height belongs to the terrain it was read from: another globe or
// another terrain provider is another surface, however settled the camera.
const provider = viewer.terrainProvider;
if (globe !== globeRead || provider !== providerRead) {
clear();
globeRead = globe;
providerRead = provider;
}
return globe;
}
/**
* Look at the camera and the globe's tile loading without asking for a
* height. The surface only sees a change when it is asked something, so a
* caller that asks nothing while tiles load (it waits for them) calls this
* on its own walk; otherwise the load would pass unseen and the heights
* kept from before it would never be read again.
* @param {object} viewer
*/
function observe(viewer) {
const globe = terrainGlobe(viewer);
if (globe) noteChange(viewer, globe, performance.now());
}
/**
* The loaded terrain's height at a point, in metres above the ellipsoid, or
* undefined when it is not known yet or the viewer has no terrain globe
* showing. A point read within the reuse time answers from memory, and so
* does one read after the camera and the globe's tiles had settled: the
* surface under it cannot have changed since. Past the window's budget a
* kept point keeps its last height and a new point borrows its floor
* cell's. Requests tend to come in the same order every pass, so re-reads
* go to the longest-kept points first: otherwise the first few would be
* re-read forever and the rest never.
* @param {object} viewer The Cesium viewer whose globe is read.
* @param {number} lat Degrees.
* @param {number} lon Degrees.
* @param {number} [need] How much the answer matters; see TERRAIN_NEED.
*/
function height(viewer, lat, lon, need = TERRAIN_NEED.PATH) {
const globe = terrainGlobe(viewer);
if (!globe) return undefined;
const now = performance.now();
if (now - windowAt >= TERRAIN_READ_WINDOW_MS) {
for (let i = 0; i < deferredAt.length; i++) {
refreshUpTo[i] = deferredAt[i];
deferredAt[i] = Infinity;
}
windowAt = now;
readMs = refreshMs = 0;
noteChange(viewer, globe, now);
}
const key = gridKey(lat, lon, 1e5);
const held = heights.get(key);
if (
held &&
(now - held.at < TERRAIN_HEIGHT_FRESH_MS ||
held.at >= changedAt + TERRAIN_SETTLE_MS)
) {
counters.reused++;
return held.h;
}
const cellKey = gridKey(lat, lon, 1e3);
const marker = need === TERRAIN_NEED.MARKER;
// Background may use a quarter of each budget; drawn geometry the rest.
const share = need === TERRAIN_NEED.BACKGROUND ? 0.25 : 1;
const affordable =
marker && !held
? true
: readMs < TERRAIN_READ_BUDGET_MS * (marker ? 1 : share) &&
(!held ||
marker ||
(refreshMs < TERRAIN_REFRESH_BUDGET_MS * share &&
held.at <= refreshUpTo[need]));
if (!affordable) {
counters.deferred++;
if (held) {
if (!marker && held.at < deferredAt[need]) deferredAt[need] = held.at;
waiting++;
return held.h;
}
const borrowed = cells.get(cellKey);
if (borrowed !== undefined) {
counters.borrowed++;
return borrowed;
}
waiting++;
return undefined;
}
Cesium.Cartographic.fromDegrees(lon, lat, 0, point);
const rendered = globe.getHeight(point);
const spent = performance.now() - now;
readMs += spent;
if (held) refreshMs += spent;
counters.reads++;
// A tile that left the globe keeps the height it gave.
if (!Number.isFinite(rendered)) return held?.h;
if (held) {
held.h = rendered;
held.at = now;
} else {
if (heights.size >= TERRAIN_HEIGHT_CACHE_MAX) {
let drop = TERRAIN_HEIGHT_CACHE_MAX >> 2;
for (const old of heights.keys()) {
heights.delete(old);
if (--drop <= 0) break;
}
cells.clear();
}
heights.set(key, { h: rendered, at: now });
}
cells.set(cellKey, rendered);
return rendered;
}
return {
height,
observe,
/**
* Requests so far that were left unread or due a re-read. A caller that
* reads it before and after its own pass learns whether to come back.
*/
waitingCount: () => waiting,
/** Read counters since creation, for checks. */
stats: () => ({ ...counters, held: heights.size }),
clear,
};
}
+154
View File
@@ -0,0 +1,154 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import {
TERRAIN_NEED,
TERRAIN_READ_BUDGET_MS,
TERRAIN_READ_WINDOW_MS,
TERRAIN_SETTLE_MS,
createTerrainSurface,
} from './terrainSurface.js';
/** A viewer over a terrain globe whose every read costs `costMs` of a clock the test owns. */
function world(t, { costMs = 0, surface = () => 100 } = {}) {
const clock = { now: 5_000_000 };
t.mock.method(performance, 'now', () => clock.now);
const reads = [];
const model = { surface, costMs };
const globe = () => ({
show: true,
getHeight(point) {
const lat = Cesium.Math.toDegrees(point.latitude);
const lon = Cesium.Math.toDegrees(point.longitude);
reads.push(`${lat.toFixed(5)},${lon.toFixed(5)}`);
clock.now += model.costMs;
return model.surface(lat, lon);
},
});
const viewer = {
scene: { globe: globe() },
camera: {
positionCartographic: { longitude: 0, latitude: 0, height: 900 },
},
};
return { viewer, clock, reads, model, globe };
}
test('a viewer without a terrain globe showing has no terrain height', (t) => {
const { viewer, reads } = world(t);
const surface = createTerrainSurface();
viewer.scene.globe.show = false;
assert.equal(surface.height(viewer, 42, -71), undefined);
assert.equal(surface.height({ scene: {} }, 42, -71), undefined);
assert.equal(surface.height(null, 42, -71), undefined);
assert.equal(reads.length, 0);
});
test('every caller of one surface shares its heights and its budget', (t) => {
const costMs = 2;
const { viewer, reads, clock } = world(t, { costMs });
const surface = createTerrainSurface();
// Two layers asking about the same street: the second reads nothing.
const transit = () => surface.height(viewer, 42.36, -71.06);
const rail = () => surface.height(viewer, 42.36, -71.06);
assert.equal(transit(), 100);
assert.equal(rail(), 100);
assert.equal(reads.length, 1);
// And what one spends in a window the other cannot spend again.
const affordable = TERRAIN_READ_BUDGET_MS / costMs;
let lon = -72;
const fresh = () => surface.height(viewer, 42, (lon -= 0.001));
for (let i = 0; i < affordable - 1; i++) assert.equal(fresh(), 100);
const before = surface.waitingCount();
assert.equal(fresh(), undefined, 'the window is spent, whoever asks');
assert.equal(surface.waitingCount(), before + 1);
assert.equal(
surface.height(viewer, 43, -73, TERRAIN_NEED.MARKER),
100,
'a marker is still read',
);
clock.now += TERRAIN_READ_WINDOW_MS;
assert.equal(fresh(), 100);
assert.deepEqual(surface.stats(), {
reads: affordable + 2,
reused: 1,
deferred: 1,
borrowed: 0,
held: affordable + 2,
});
});
test("another globe's surface is read afresh", (t) => {
const { viewer, reads, clock, model, globe } = world(t, {
surface: () => 10,
});
const surface = createTerrainSurface();
assert.equal(surface.height(viewer, 42.36, -71.06), 10);
clock.now += TERRAIN_SETTLE_MS;
assert.equal(surface.height(viewer, 42.36, -71.06), 10);
clock.now += 60_000;
model.surface = () => 250;
assert.equal(surface.height(viewer, 42.36, -71.06), 10, 'settled: kept');
// The map source changed and the viewer has a new globe.
viewer.scene.globe = globe();
assert.equal(surface.height(viewer, 42.36, -71.06), 250);
assert.equal(reads.length, 3);
assert.equal(surface.stats().held, 1);
});
test('another terrain provider is read afresh, however settled the old heights were', (t) => {
const { viewer, reads, model, clock } = world(t);
const surface = createTerrainSurface();
viewer.terrainProvider = {};
const at = () => surface.height(viewer, 42.36, -71.06);
// Read once the camera and tiles have long settled: kept indefinitely.
surface.height(viewer, 10, 10);
clock.now += TERRAIN_SETTLE_MS * 2;
reads.length = 0;
assert.equal(at(), 100);
clock.now += TERRAIN_SETTLE_MS * 10;
model.surface = () => 250;
assert.equal(at(), 100, 'same terrain: the kept height stands');
assert.equal(reads.length, 1);
const real = (viewer.terrainProvider = {});
assert.equal(at(), 250, 'a new provider is a new surface');
assert.equal(reads.length, 2);
// Terrain → flat → the same provider. A caller answers a flat globe
// itself and asks nothing, so it shows the flat provider with observe().
viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider();
surface.observe(viewer);
model.surface = () => 30;
viewer.terrainProvider = real;
assert.equal(at(), 30);
assert.equal(reads.length, 3);
});
test('observe() lets a caller that asks nothing while tiles load report the load', (t) => {
const { viewer, reads, clock, model } = world(t, { surface: () => 10 });
const surface = createTerrainSurface();
const read = () => surface.height(viewer, 42.36, -71.06);
read();
clock.now += TERRAIN_SETTLE_MS;
read();
clock.now += 60_000;
assert.equal(reads.length, 2, 'settled and kept');
// Finer tiles load and finish while nobody asks for a height.
model.surface = () => 14;
viewer.scene.globe.tilesLoaded = false;
clock.now += 1_000;
surface.observe(viewer);
viewer.scene.globe.tilesLoaded = true;
clock.now += 3_000;
assert.equal(
read(),
14,
'the height kept from before the load is read again',
);
assert.equal(reads.length, 3);
surface.observe(null);
surface.observe({ scene: {} });
});
-1
View File
@@ -16,5 +16,4 @@ export {
createFlightSource,
createMilitarySource,
createVesselSource,
fetchOpenVesselPhoto,
} from './standalone.js';
-38
View File
@@ -12,11 +12,6 @@ import {
} from './aircraft.js';
import { normalizeVesselTrack, vesselSnapshot } from './vessels.js';
import { parsePlanespottersPhoto } from '../../data/planespottersPhoto.js';
import {
MMSI,
openVesselPhotosUrl,
parseOpenVesselPhoto,
} from '../../data/openVesselPhoto.js';
const defaultFetch = (...args) => globalThis.fetch(...args);
const header = (response, name) => response.headers?.get?.(name);
@@ -47,34 +42,6 @@ async function planespottersPhoto(fetchImpl, hex, signal) {
return parsePlanespottersPhoto(payload);
}
/**
* A vessel's Open Vessel Photos photo, read and host-checked. The API is
* keyless and answers with `access-control-allow-origin: *`, so the browser
* asks it straight. An MMSI it does not know answers 404: no photo.
* @param {Function} fetchImpl fetch.
* @param {string} mmsi The vessel's nine-digit MMSI.
* @param {AbortSignal} [signal] Abort signal.
* @returns {Promise<object>} A `parseOpenVesselPhoto` result.
*/
export async function fetchOpenVesselPhoto(fetchImpl, mmsi, signal) {
const id = String(mmsi || '');
if (!MMSI.test(id))
throw new LiveSourceError('unsupported', 'Enrichment unavailable');
const timeout = AbortSignal.timeout(PHOTO_TIMEOUT_MS);
const { response, payload } = await readResponse(
fetchImpl,
openVesselPhotosUrl(id),
{
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
credentials: 'omit',
},
'Open Vessel Photos',
);
if (response.status === 404) return parseOpenVesselPhoto(null);
if (!response.ok) throw httpError(response, 'Open Vessel Photos');
return parseOpenVesselPhoto(payload, id);
}
function openSkyError(response) {
const error = httpError(response, 'Flights');
const mode = String(
@@ -268,11 +235,6 @@ export function createVesselSource({
status: response.status,
};
},
async getEnrichment(query, { signal } = {}) {
if (query.kind !== 'photo')
throw new LiveSourceError('unsupported', 'Enrichment unavailable');
return fetchOpenVesselPhoto(fetchImpl, query.id, signal);
},
async getTrack(reference, { signal } = {}) {
const { response, payload } = await readResponse(
fetchImpl,
+14 -5
View File
@@ -6,11 +6,13 @@ import { createStandaloneScene } from './scene.js';
import { createStandaloneControls } from './controls.js';
import { createStandaloneData } from './data.js';
import { createStandaloneTools } from './tools.js';
import { resolveLocale, applyDocumentLanguage } from '../i18n/locale.js';
import {
applyDocumentTranslations,
followPageLocale,
startPageLocale,
} from '../i18n/page.js';
getLocale,
onLocaleChange,
setLocale,
} from '../i18n/index.js';
import { mountApplicationViews } from '../ui/views/index.js';
import { createGoogleTokenSource } from '../maps/googleTokens.js';
@@ -35,7 +37,9 @@ export function createStandaloneApplication({
// scripts execute after parsing, so the DOM is ready here: the views
// translate as they render, and the page's static data-i18n* markup (the
// loading screen) is translated here and after every language switch.
startPageLocale(document);
const activeLocale = resolveLocale();
setLocale(activeLocale);
applyDocumentLanguage(document, activeLocale);
// Views render the page's markup; wiring reads it.
mountApplicationViews(document);
applyDocumentTranslations(document);
@@ -46,7 +50,12 @@ export function createStandaloneApplication({
return createApplication({
createScene: async (context) => {
// A language switch applies in place; the page's own markup follows.
context.defer(followPageLocale(document));
context.defer(
onLocaleChange(() => {
applyDocumentLanguage(document, getLocale());
applyDocumentTranslations(document);
}),
);
placeSearch = createStandalonePlaceSearch({
// The bundled city and landmark data the offline name provider reads.
// The search package takes it as plain data rather than importing it,
+2 -2
View File
@@ -96,7 +96,7 @@ export class StyleManager extends ShellFacade {
* @param {Cesium.Viewer} viewer - The CesiumJS viewer instance.
* @param {object} [options]
* @param {boolean} [options.localePicker=true] - false removes the language
* picker, for a host that offers its own language choice.
* picker, for a host that does not initialize Core's locale.
*/
constructor(
viewer,
@@ -764,7 +764,7 @@ export class StyleManager extends ShellFacade {
this._lifetime.own(
onLocaleChange(() => nextTick(() => this._sizeDisplayLabelColumn())),
);
// A host that offers its own language choice shows no picker.
// A host that does not initialize Core's locale shows no picker.
if (this._localePickerEnabled === false) return;
this._publishLocalePicker();
}
-45
View File
@@ -1,7 +1,6 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { CctvControls } from './cctvControls.js';
import { CCTV_FRAME_DECODE_WAIT_MS } from './cctvFrames.js';
function element() {
const classes = new Set();
@@ -60,50 +59,6 @@ test('a late image completion cannot replace a newer camera preview', (t) => {
assert.equal(controls._frame.cameraId, 'b');
});
test('a camera frame settles once decoded, and a stale decode cannot land', async (t) => {
const { controls, requests } = fixture(t);
const decodes = [];
const queue = (src, id) => {
controls._queueCctvFrame(src, id, true);
const request = requests.at(-1);
request.decode = () =>
new Promise((resolve) => decodes.push({ src, resolve }));
return request;
};
const first = queue('first.jpg', 'a');
first.onload();
assert.equal(controls._frame.hasFrame, false);
const second = queue('second.jpg', 'b');
second.onload();
decodes[1].resolve();
await Promise.resolve();
decodes[0].resolve();
await Promise.resolve();
assert.equal(controls._frame.src, 'second.jpg');
assert.equal(controls._frame.cameraId, 'b');
assert.equal(controls._frame.hasFrame, true);
});
test('a loaded frame shows when its decode rejects or never settles', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const { controls, requests } = fixture(t);
controls._queueCctvFrame('broken.jpg', 'a', true);
requests[0].decode = () => Promise.reject(new Error('EncodingError'));
requests[0].onload();
await Promise.resolve();
await Promise.resolve();
assert.equal(controls._frame.src, 'broken.jpg');
assert.equal(controls._frame.hasFrame, true);
controls._queueCctvFrame('hidden.jpg', 'b', true);
requests[1].decode = () => new Promise(() => {});
requests[1].onload();
assert.equal(controls._frame.hasFrame, false);
t.mock.timers.tick(CCTV_FRAME_DECODE_WAIT_MS);
assert.equal(controls._frame.src, 'hidden.jpg');
assert.equal(controls._frame.hasFrame, true);
});
test('failed refresh preserves settled pixels, but changing cameras clears them', (t) => {
const { controls, requests } = fixture(t);
controls._queueCctvFrame('first.jpg', 'a', true);
+1 -25
View File
@@ -77,9 +77,6 @@ export function cctvSourceLabelText(label) {
return SOURCE_LABELS[text] ? t(SOURCE_LABELS[text]) : text;
}
/** Longest wait for a loaded frame's decode before it is shown anyway. */
export const CCTV_FRAME_DECODE_WAIT_MS = 1000;
/** The preview before any frame: nothing requested, nothing shown. */
export const EMPTY_CCTV_FRAME = Object.freeze({
cameraId: '',
@@ -130,28 +127,7 @@ export function _queueCctvFrame(src, cameraId, cameraChanged) {
const preloader = new Image();
this._cctvFramePreloader = preloader;
// Settle once the image is decoded, not merely loaded: the panel's <img>
// then paints the frame the moment it takes the src, instead of showing
// its empty box for the decode.
// A decode that rejects after a load still has drawable data, as it was
// before decoding was awaited. A decode that never settles (a hidden tab may
// hold it until the page draws) shows the loaded frame after
// CCTV_FRAME_DECODE_WAIT_MS rather than leaving the preview loading.
preloader.onload = () => {
if (typeof preloader.decode !== 'function') {
this._settleCctvFrame(token, src, true);
return;
}
let settled = false;
const settle = () => {
if (settled) return;
settled = true;
clearTimeout(timer);
this._settleCctvFrame(token, src, true);
};
const timer = setTimeout(settle, CCTV_FRAME_DECODE_WAIT_MS);
preloader.decode().then(settle, settle);
};
preloader.onload = () => this._settleCctvFrame(token, src, true);
preloader.onerror = () => this._settleCctvFrame(token, src, false);
preloader.src = src;
}
+1 -1
View File
@@ -627,7 +627,7 @@ test('Authoring PLAY uses the scene context, not the radio PLAY', async (context
const f = fixture();
try {
await nextTick();
assert.equal(f.elements.start.textContent, 'REPRODUCIR');
assert.equal(f.elements.start.textContent, 'REPROD.');
} finally {
f.restore();
}
+1 -4
View File
@@ -250,11 +250,8 @@ html:not([lang='en']) .cctv-controls .scene-btn {
height: auto;
object-fit: contain;
display: block;
/* No fade: a camera change hides the old frame at once (never its pixels
under the new camera) and shows the new one as soon as it is decoded. A
180 ms fade-in after every arriving frame made each camera click linger
on black. */
opacity: 0;
transition: opacity 180ms ease;
}
#cctv-video {
+1 -1
View File
@@ -570,7 +570,7 @@ test('street-level failure cards are in the interface language; speech stays Eng
]);
assert.deepEqual(
lines('Google Street View needs the photoreal map, which is not available', { stage: 'map' }),
['Google Street View braucht die Karte „Google 3D“, die nicht verfügbar ist'],
['Google Street View braucht die Google 3D-Karte, die nicht verfügbar ist'],
);
assert.deepEqual(lines('Mapillary could not be switched on', { stage: 'provider' }), [
'Mapillary konnte nicht eingeschaltet werden',