diff --git a/.env.example b/.env.example index 0876262b2..c9700c5be 100644 --- a/.env.example +++ b/.env.example @@ -256,6 +256,8 @@ AISSTREAM_API_KEY= # CCTV_TAIWAN_ENABLED=1 # set 0 to skip Taiwan Freeway Bureau (MOTC) road cameras # CCTV_TAIWAN_MAX_SOURCES=600 # cap Taiwan freeway cameras (1-2000); no API key required # CCTV_TAIWAN_URL= # override the Freeway Bureau MOTC 2.0 CCTV.xml URL +# CCTV_A22_ENABLED=1 # set 0 to disable the Autostrada del Brennero (A22, Italy) webcams +# CCTV_A22_URL= # override the Open Data Hub A22 webcam catalog URL # CCTV_MAX_SOURCES=4000 # catalog-wide ceiling, shared round-robin across packs # CCTV_PREFER_AUSTIN=1 # prioritize Austin cameras in the registry # CCTV_FORCE_AUSTIN= # force live default sources when a source-pack JSON is configured diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index e31d0d3f0..a4035f175 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -69,6 +69,7 @@ How to read this: | **Kartverket** (`ws.geonorge.no/hoydedata/v1/punkt`) | Terrain heights (NN2000) used as the ground prior for Statens vegvesen cameras | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — attribution REQUIRED | "© Kartverket", modified (heights over water set to 0 m, rounded to 0.1 m) | | **NYC DOT Traffic Management Center** (`webcams.nyctmc.org`) | CCTV camera catalog + frames, New York City five boroughs | NYC DOT requires developers to sign a [data-sharing agreement](https://www.nyc.gov/html/dot/html/about/datafeeds.shtml) for the camera feed. **Off by default**; enable only under your own agreement | "NYC DOT Traffic Management Center" + NYC DOT's required application disclaimer (in the Data attribution popover) | | **City of Toronto Open Data** (`opendata.toronto.ca`) | CCTV camera catalog + frames, Toronto city streets | [Open Government Licence – Toronto](https://www.toronto.ca/city-government/data-research-maps/open-data/open-data-licence/) — attribution REQUIRED | "Contains information licensed under the Open Government Licence – Toronto" | +| **Autostrada del Brennero (A22)** (catalog `tourism.api.opendatahub.com`, frames `www.autobrennero.it/WebCamImg/`) | CCTV camera catalog + frames, A22 Brenner–Modena motorway, Italy | Public Autobrennero webcam images; camera list republished by the Open Data Hub, whose records are tagged CC0 (`ClosedData: false`, licence holder Autobrennero); Autobrennero publishes no terms of its own for the images | "Webcam images © Autostrada del Brennero S.p.A., via the Open Data Hub (NOI Techpark)" (courtesy) | | **GBFS (Lyft / BCycle / Social Bicycles / PBSC)** | Bikeshare availability | Per-feed (attribution-only) | Credit the operator (e.g. Austin BCycle) + its `license_url` | | **Bike Share Toronto** (`toronto.publicbikesystem.net`) | Bikeshare station availability, Toronto | Operator GBFS feed. No licence is published: the City's open-data package declares none and the feed has no `license_url`. Attribution is given to the operator. | "Bike Share Toronto (Toronto Parking Authority)" | | **Bike Itaú / Tembici** (`*.publicbikesystem.net`, seven Brazilian cities, Santiago and Bogotá) | Bikeshare station availability: São Paulo, Rio de Janeiro, Salvador, Recife, Porto Alegre, Brasília, Curitiba, Santiago de Chile and Bogotá | Operator GBFS feeds. No licence is published: the feeds' `system_information.json` carries no `license_url`, operator or attribution fields. Attribution is given to the operator. | "Bike Itaú (Tembici)"; "Tembici Bogotá" | @@ -176,6 +177,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei - **Kartverket ground heights for Statens vegvesen cameras.** The camera feed's points are 2D, so the loader asks Kartverket's keyless point-height API (`ws.geonorge.no/hoydedata/v1/punkt`, CC BY 4.0) for the terrain height at each camera. Requests carry 20 points: the API accepts 50, but above ~20 it answers the extra points from contour lines (`hoydekurver`, metres to ~20 m off) or not at all. Eight requests run at a time, about 36 for the whole country: a cold lookup took 6–13 s in testing (single requests 0.6–9.5 s). Answers are matched by the coordinates the API echoes and recorded only for points that request asked about. Over water the API returns the sea-floor depth (`dybdekurver` or `Havflate`), which is replaced by 0 m, the water surface; for the ~14 bridge cameras that is below the deck, but much closer than the sea floor or the flat prior. Heights are cached in memory by position for the life of the server, and points with no height (a few ferry quays and bridges, or a point a 200 answer leaves out) for 6 h, so the 15-minute catalog refresh only asks about new or moved cameras. The first failing request (a 408, 429 or 5xx, a network error, a request over 10 s, or an unreadable or oversized body) stops the lookup, with no retries, and no new lookup starts until the backoff ends (Retry-After when sent, else 30 min, longer than the refresh; bounded to 1 min–1 h). Any other 4xx marks that request's points as having no height and the lookup carries on. The whole lookup has a 30 s deadline; a lookup cut short by it resumes on the next refresh without a backoff. The lane waits at most 12 s, counted from the start of the loader, and only when more than 10% of the cameras have no answer yet; a few new cameras or expired misses are looked up in the background. A slower lookup keeps filling the cache, and the cameras it had not reached keep the flat prior until the next 15-minute refresh (about 190 of 865 on one slow first run). Only coordinates are sent, with the same User-Agent as the other CCTV requests. `CCTV_VEGVESEN_HEIGHTS=0` turns the lookup off. The Kartverket credit, with a note that heights over water are set to 0 m and heights are rounded to 0.1 m, is shown beside the Statens vegvesen entry in the Data attribution popover. - **NYC DOT Traffic Management Center cameras.** Off by default. NYC DOT's [developer data feeds page](https://www.nyc.gov/html/dot/html/about/datafeeds.shtml) asks developers who want the camera feed to contact `tmcdot@dot.nyc.gov` and sign a data-sharing agreement. This project does not hold one that covers downstream installations, so the pack loads only when an operator who holds their own agreement sets `CCTV_NYC_ENABLED=1`; with the flag unset, empty or any other value, the server makes no request to NYC DOT. The endpoint needs no key. The camera list is the public `webcams.nyctmc.org/api/cameras` catalog (~980 cameras across the five boroughs); frames are stills at `webcams.nyctmc.org/api/cameras//image`, synthesized from a validated UUID rather than taken from the payload, and only that host is registered. Cameras the feed reports as offline are dropped. The catalog publishes no camera facing, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. Once enabled, the 250 cameras nearest the borough cores load; `CCTV_NYC_MAX_SOURCES` (8–400) changes the cap. Frames are fetched at request time and never stored or redistributed. NYC DOT asks every application that uses its data to carry its disclaimer; it is registered verbatim in the Data attribution popover. - **City of Toronto RESCU cameras.** Keyless. The camera list is the City's public `tmcearthcameras.json` on `opendata.toronto.ca` (dataset "Traffic Cameras", Transportation Services), served as JSONP and unwrapped with an anchored pattern. This is the municipal network the Ontario 511 pack does not carry: downtown arterials plus the Gardiner, DVP, Allen and Lake Shore. The list publishes no frame URL, so frame URLs are BUILT from the documented `CameraImages/loc####.jpg` convention and a strictly-digits camera number, pinning the frame proxy to the portal origin by construction; the list fetch refuses redirects and bounds the body it buffers. Frames are fetched at request time and never stored or redistributed. The dataset publishes no camera facing: the `D1`-`D4` fields are the directions of the *comparison* images (static reference stills captured in 2016 under a different path), not a bearing for the live pan-tilt-zoom frame, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. A camera that is temporarily out of service still answers 200 with a byte-identical "Image Unavailable" placeholder rather than an error (about 6% of the network at any moment), so such a camera reads as healthy; the frame it shows is the agency's, not a synthesized one. The licence requires the attribution statement above. Disable with `CCTV_TORONTO_ENABLED=0`. +- **Autostrada del Brennero (A22) webcams.** Keyless. The camera list is one request to the Open Data Hub tourism API (`tourism.api.opendatahub.com/v1/WebcamInfo?source=a22`), run by NOI Techpark in South Tyrol, which republishes Autobrennero's own list of 13 motorway webcams with coordinates and kilometre markers. Frames are the operator's stills on `www.autobrennero.it/WebCamImg/km.jpg`, refreshed about once a minute, and only the exact image for each camera's kilometre is registered. The feed names no bearing, so headings use the id-hash fallback at low confidence. The road climbs from about 20 m in the Po valley to about 1,370 m at the Brenner Pass, so each camera's ground elevation comes from a per-kilometre table sampled from the Copernicus GLO-90 DEM (via Open-Meteo's elevation API); a camera not in the table uses a 200 m prior. The Open Data Hub tags every record CC0 (`ClosedData: false`, Autobrennero as licence holder), but that tag is the aggregator's metadata and Autobrennero publishes no terms of its own for the images, so they are credited to the operator as a courtesy and are not redistributed. Frames are fetched at request time and never stored. Disable with `CCTV_A22_ENABLED=0`. - **Camera frame content.** Public camera frames can contain people, vehicles and license plates, and the camera layer does nothing with them beyond putting them on the map. A successful upstream response is relayed as the provider served it: nothing in the camera pipeline blurs, redacts, enhances or restores it, and nothing there runs face, plate or object detection or otherwise recognises or classifies what is in the picture. (The ALPR layer described below maps where plate-reader cameras are, from locations contributors tagged in OpenStreetMap; it reads nothing from any camera frame.) When an upstream has no frame for a camera, what you see instead is a Google Street View still or the app's own placeholder card; the panel's source badge names which of them you are looking at, and a refresh that fails after a frame has arrived leaves the last good picture on screen rather than replacing it. The browser resamples each frame for display: onto a fixed 1920x1080 surface for the camera's projection plane in the scene, and onto a 192x108 canvas for the small ambient card. That scaling is the only change made to the picture, and the only pixels the camera layer reads back are a 64x36 downscale hashed to tell whether the picture has changed. Live streams are piped through; the still path reads the image into memory to pass it on — TxDOT wraps its JPEG in a JSON envelope, which is decoded there — and no frame is written to disk (the disk cache, `.gev-cache/`, holds feed JSON for other layers and never camera imagery). Outside that pipeline, the voice assistant is the one feature that sends imagery anywhere: when it answers a question about what is in view it may attach a downscaled screenshot of the rendered scene — including any camera plane on screen — to the request it sends to the voice model. Frames are fetched only from server-registered camera URLs — a client cannot hand the proxy a URL (see [SECURITY.md](SECURITY.md)). What the app does stays inside the line README draws: no named-person search, no face recognition, no tracking individuals. - **Transit (GTFS-Realtime).** The proxy requests only registered operator feeds, validates redirects, bounds responses, and uses conditional requests, short snapshot caching and failure backoff. All sixteen registered regions are live-enabled; each can be disabled independently. Vehicles use delayed playback between reports, with report age and waiting states shown separately. Session-local selected trails are available for every feed. MBTA additionally retains up to 15 minutes of recently observed positions in the running proxy, so available history can survive a browser reload; server restart clears it. History reads never fetch upstream, and other feeds have no proxy retention. Nothing is archived to disk or collected in the background. Operator text credits are included as a courtesy and do not imply endorsement. Heights are aligned to work with Google 3D tiles; subway markers are shown at street level because these feeds do not supply depth. The TTC feed is BusTime, the surface system: it carries buses and streetcars and no subway at all. About four in ten of its vehicles report no route at all (unassigned or deadheading), and those are shown with the feed default rather than a route-established mode. It sends no ETag or Last-Modified, so its conditional requests fall through to the ordinary snapshot cache. Three Golden Horseshoe neighbours run alongside it — MiWay, Hamilton HSR and Durham Region Transit — each bus-only and each polled only near its own region. Durham Region Transit publishes no bearing on any vehicle, so those sprites take their heading from observed movement like any other feed that omits it. Guelph, Kingston and Barrie extend the same pattern eastward and north. Hamilton, Guelph, Burlington and YRT publish route ids that are not the number on the vehicle, so those entries carry a route-name table taken from each operator’s own published GTFS; any id missing from a table renders unchanged rather than blank. A label is never invented: YRT runs a route `9101` that appears nowhere in its published routes, so it stays as the feed sends it. Burlington’s is the one that cannot be shortcut: `351` is route 1 while `3510` is route 10, so the table is a real join rather than a trimmed prefix. GO Transit / Metrolinx is deliberately absent: its REALTIME feed requires an API key issued by manual review, which the keyless admission rule excludes (its static schedule download is keyless, but carries no vehicle positions). Barrie and YRT/Viva each invite acceptance of their licence through a form, and each licence also states that using the data is itself acceptance; this deployment relies on that clause, which is recorded in both entries. Grand River Transit in Waterloo is absent for a different reason: it answers, but its feed header was months stale and it publishes no per-vehicle timestamps, so it would render vehicles that look live and are not. Ottawa’s OC Transpo falls to the same keyless admission rule as GO Transit — its realtime feed requires an API key. London Transit and Thunder Bay Transit publish vehicle positions over plain HTTP with no HTTPS endpoint available. Every other Ontario system — Brampton, Oakville, Niagara, Milton, Windsor, Greater Sudbury, UP Express and a dozen smaller operators — publishes static GTFS schedules only, with no realtime vehicle positions to register. Between the nine included and these exclusions, every Ontario realtime vehicle-position feed catalogued in the Mobility Database is accounted for. - **Radio Browser.** `/api/radio/stations` discovers official API mirrors, makes bounded and coalesced healthy/geolocated HTTPS-station queries, caches the normalized public-domain directory for 45 minutes, and may serve the last good catalog for up to seven days during an outage. Refreshes must meet minimum accepted-query and station coverage before replacing a warm catalog; schema-valid responses whose rows all fail the product's health policy do not count as successful queries. A usable partial cold catalog is explicitly `DEGRADED`, and non-array or empty successful payloads are rejected atomically. Individual structurally malformed rows are skipped rather than failing the whole query; the response carries their bounded `rejectedStationCount` (at most 10,000), which the client adds to its own rejected rows and reports as degraded coverage without discarding the accepted generation. Every directory and click-count request rejects redirects, validates all resolved addresses as globally routable (including reserved/documentation IPv4 and special/non-global IPv6 exclusions), and pins the TLS connection to a validated address. Only MP3/AAC non-HLS directory rows with public HTTPS stream targets are returned; favicons are intentionally omitted. Pressing play connects one browser audio element directly to the selected broadcaster and calls the directory's click counter through known-ID-only `POST /api/radio/click/:uuid`. GEV never proxies, caches, records, bundles, or redistributes audio. Radio Browser supplies station-level tags, not dependable current-song or upcoming-program metadata, so Radio filtering never claims either. Direct playback exposes the listener's IP address to the broadcaster, whose own stream terms apply. diff --git a/README.md b/README.md index 745bec3d8..ac135360f 100644 --- a/README.md +++ b/README.md @@ -399,7 +399,7 @@ Twenty layers and map sources. **Eighteen have a keyless path.** Some offer addi | 🌍 **Earthquakes** | Global seismic activity, last 24h | USGS | 🟢 | | ◍ **Air quality** | Live AQHI on the published 1–10+ health-risk scale, with a Weather-panel card showing the band legend and the highest readings in view. **Canada only** — ECCC is the first registered provider | ECCC MSC GeoMet | 🟢 | | 🚗 **Traffic** | Simulated vehicles on OSM roads. With TomTom, live flow speeds drive the simulation and congestion colors below ~8 km; individual vehicle positions are not live observations | TomTom + OSM | 🟢 simulation · 🟡 live flow speeds | -| 📹 **CCTV Mesh** | ~4,000 public cameras (shared cap) projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511, free developer key) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary · Norway (Statens vegvesen) · Toronto · King County (WA) · Washington (WSDOT) · Catalonia (Servei Català de Trànsit) · Hong Kong (Transport Department) · Taiwan (Freeway Bureau, MOTC), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement), opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed) and opt-in Florida (FL511, individual personal use only; other use needs FDOT's written consent). Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 | +| 📹 **CCTV Mesh** | ~4,000 public cameras (shared cap) projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511, free developer key) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary · Norway (Statens vegvesen) · Toronto · King County (WA) · Washington (WSDOT) · Catalonia (Servei Català de Trànsit) · Hong Kong (Transport Department) · Taiwan (Freeway Bureau, MOTC) · Italy (A22 Brennero motorway), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement), opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed) and opt-in Florida (FL511, individual personal use only; other use needs FDOT's written consent). Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 | | 📷 **Mapped ALPR Cameras** | License-plate-reader camera locations tagged by OpenStreetMap contributors, loaded one city-sized view at a time, with **SHOW NEAREST**. Locations and tags only: no plate data, no video | OpenStreetMap (incl. DeFlock mapping) | 🟢 | | 📷 **Street Level** | Street-level photos behind one panel, one chip per provider: coverage on the streets, image cones, an embedded viewer with full-screen **EXPAND**. Mapillary, Google Street View, KartaView and Panoramax | Mapillary · Google · KartaView · Panoramax | 🟢 KartaView, Panoramax · 🟡 Mapillary token · 🔴 Google Maps key | | 📻 **Radio** | Geolocated world radio with an **analog tuner** — drag the needle across up to 750 stations and the globe flies to each broadcaster | Radio Browser / broadcasters | 🟢 | diff --git a/changelog.d/1078-firms-ipv4-fallback.md b/changelog.d/1078-firms-ipv4-fallback.md new file mode 100644 index 000000000..ff4e60e1b --- /dev/null +++ b/changelog.d/1078-firms-ipv4-fallback.md @@ -0,0 +1,4 @@ +- FIRMS fire data loads on hosts where IPv6 is unreachable and IPv4 is slow to + connect: a request whose every address fails to connect retries once over + IPv4, and FIRMS warnings name the underlying connection errors instead of + only `fetch failed` (jeffblakely, #1078). diff --git a/changelog.d/1466-panel-chrome-consistency.md b/changelog.d/1466-panel-chrome-consistency.md new file mode 100644 index 000000000..c7f1f28a1 --- /dev/null +++ b/changelog.d/1466-panel-chrome-consistency.md @@ -0,0 +1,5 @@ +- Give the side panels one look. Every panel title now uses the command dock's + label type in a brighter colour; a panel's edge and title light up on hover, + as the dock's trays do; and in the default theme each collapse button sits + 19px from its panel's edge (plus the scrollbar's width in an open, scrolling + panel where the system always shows scrollbars) (asturksever, #1466). diff --git a/changelog.d/1467-remove-operator-hud-layout.md b/changelog.d/1467-remove-operator-hud-layout.md new file mode 100644 index 000000000..556d551ee --- /dev/null +++ b/changelog.d/1467-remove-operator-hud-layout.md @@ -0,0 +1,3 @@ +- Remove the Operator HUD layout. Display > HUD > Layout and the HUD voice + action now offer Tactical, Minimal and Cyber; share links, scenes and saved + views that name Operator open in Tactical (asturksever, #1467). diff --git a/changelog.d/1471-context-panel-cleanup.md b/changelog.d/1471-context-panel-cleanup.md new file mode 100644 index 000000000..0d97cc397 --- /dev/null +++ b/changelog.d/1471-context-panel-cleanup.md @@ -0,0 +1,7 @@ +- Remove the Radio icon from the Context panel header, along with the compact + Radio player it opened; Radio stays in Context as its own section, and + Cockpit's compact Radio is unchanged. Context is also easier to read: the + Contacts / Space Missions modes are one segmented control, labels and + guidance are larger and higher contrast, the standby hint is plain text + instead of a dashed box, and the panel draws no glow halo. Cyber keeps its + own framed buttons and red titles (asturksever, #1471). diff --git a/changelog.d/1476-french.md b/changelog.d/1476-french.md new file mode 100644 index 000000000..30d0b8c89 --- /dev/null +++ b/changelog.d/1476-french.md @@ -0,0 +1,4 @@ +- Add French (fr) to the language picker, with French typography and plural + forms. With nine languages the picker now shows its search field. Based on + the translation by @abiyeenzo (#907), checked against @meirm's French + catalog (#312) (German, @gekh, #1476). diff --git a/changelog.d/1477-german.md b/changelog.d/1477-german.md new file mode 100644 index 000000000..77d7717b6 --- /dev/null +++ b/changelog.d/1477-german.md @@ -0,0 +1,3 @@ +- Add German (de) to the language picker, with a full translation of the + interface and a German style guide in docs/translations/de.md. Based on + translations by @abiyeenzo (#908) (German, @gekh, #1477). diff --git a/changelog.d/1478-japanese.md b/changelog.d/1478-japanese.md new file mode 100644 index 000000000..376853e24 --- /dev/null +++ b/changelog.d/1478-japanese.md @@ -0,0 +1,4 @@ +- Add Japanese (ja) to the language picker, with year-first dates, Japanese + counters, unit words joined to their numbers (5分前, 1時間5分) and CJK label + rules for Japanese text. Based on translations by @abiyeenzo (#913) + (German, @gekh, #1478). diff --git a/changelog.d/1485-cctv-view-thinning.md b/changelog.d/1485-cctv-view-thinning.md new file mode 100644 index 000000000..307da84ad --- /dev/null +++ b/changelog.d/1485-cctv-view-thinning.md @@ -0,0 +1,4 @@ +- Draw only the CCTV cameras in view, and thin them to one per ground cell when + zoomed out. Dragging the globe with the default 4,000-camera catalog no + longer stalls, and the camera picker lists the cameras in view with a count + of the ones outside it (samehkhamis, #1485). diff --git a/changelog.d/956-a22-brennero-webcams.md b/changelog.d/956-a22-brennero-webcams.md new file mode 100644 index 000000000..bee2545de --- /dev/null +++ b/changelog.d/956-a22-brennero-webcams.md @@ -0,0 +1,8 @@ +- Add the Autostrada del Brennero (A22, Brenner–Modena) motorway webcams as a + CCTV pack, the first Italian cameras. The 13-camera list is one keyless + request to the Open Data Hub (NOI Techpark) tourism API, which republishes + Autobrennero's own list with coordinates; frames are the operator's stills, + refreshed about once a minute and pinned to each camera's own `km.jpg`. + Each camera's ground elevation comes from a per-kilometre DEM table, since + the road climbs from the Po valley to the Brenner Pass. `CCTV_A22_ENABLED=0` + disables the pack (Jibran Tahir, @jibraaan, #956). Closes #155. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 76fb693d9..33179d661 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -221,7 +221,7 @@ from 0–200%. OFF disables the slider while retaining its value. HUD layout changes preserve both settings; share links and scenes restore them. The historical missing-`scf` fallback remains 35%; explicit 0% keeps the hard edge. -With Scope OFF, Tactical, Operator and Minimal add text-sized black backplates +With Scope OFF, Tactical and Minimal add text-sized black backplates at 70% opacity behind telemetry, vertical metadata and scene chrome. Original HUD text color, glow and opacity are preserved. Scope OFF brightens the tagline and Active Style label with the shared secondary text color. The title plate @@ -231,6 +231,15 @@ the original HUD presentation; Cyber retains its existing skin. `node scripts/qa-scope-hud.mjs` checks native input, range, layout persistence, restoration and both Scope presentations. `QA_SCREENSHOT_DIR` saves screenshots. +## HUD layouts — October 29, 2026 + +Display > HUD > Layout offers Tactical, Minimal and Cyber; the `set_hud` voice +action accepts the same three. The Operator layout is retired. Share links +keep its enum code so older links still parse, and a link, scene or saved state +that names Operator restores the default Tactical layout. Operator was the +only layout that showed the HUD's top banner bar, so its markup and styles are +removed too. + ## Cyber HUD — September 30, 2026 Display > HUD > Layout includes Cyber, also available through the HUD voice @@ -246,8 +255,8 @@ red hover border in both map and cockpit, so clipping does not hide the highligh Cyber's map-side right rail opens one panel at a time while keeping collapsed launchers available. Display, CCTV and Context scroll their contents inside fixed headers and decorative frames. The narrow-screen rail remains scrollable to reach -each panel. Radio retains the shared nested Context player and compact -header disclosure, without relocating playback controls on theme changes. +each panel. Radio retains the shared nested Context player without relocating +playback controls on theme changes. Voice help/error popups and Location/Visual Presets pins extend beyond their Cyber frames without being clipped: the shapes decorate non-interactive @@ -969,6 +978,15 @@ directory. Every `/api/cctv` route refuses cross-site browser requests but accepts forwarding headers, so cameras work behind a reverse proxy; a forwarded request skips the key-bearing Street View fallback and gets the placeholder. +The CCTV layer draws only the cameras on or near the screen. Zoomed out, it +keeps one camera per ground cell sized to the view, preferring a healthy feed, +and the cells are anchored to the ground so a pan keeps the same cameras. The +active camera always shows. Ground placement and coverage geometry load only +for the cameras in view, with at most two attempts per camera and view regime. +The view pass reruns while the camera moves, paced by its own cost. The camera +picker lists the cameras in view, active camera first, and closes with the +count of cameras outside the view. + Traffic owns its rendered-surface observer only while enabled, removes it on disable/destroy, and caches surface identity without allocating on unchanged frames. Traffic and bikeshare expose factories through `./layers/traffic` and @@ -1510,10 +1528,6 @@ Automatic collapse remains presentation only, prioritizes the latest explicit panel, and honors keyboard focus on the right. Mobile layout still releases desktop allocation styles. Stable Display allocation avoids unnecessary style writes. This extraction does not change panel positions or layout defaults. -Context expansion through the panel owner or adaptive right-rail restoration -closes an open compact Radio disclosure before Context/Space Missions becomes -visible. The mini remains available while Context is collapsed. This fixes the -Radio-overlap path reported in #804 without changing z-index or layout policy. The right rail skips panels hidden by other CSS (computed `display: none`), but keeps the collapsed panels its own exclusive presentation hides, so automatic collapse can still be undone. @@ -2002,7 +2016,7 @@ they do not reduce the materialized entity count or establish an FPS gain. rotation and mission-marker highlight as pointer hover. Focus alone does not select the mission; Enter or Space performs selection. Keyboard and pointer preview ownership remain independent when the pointer is parked over the list. -- Radio power controls in the full, compact, and Cockpit surfaces, Search Nearby +- Radio power controls in the full and Cockpit surfaces, Search Nearby Sites, and Clear Selected Layers remain focusable while lifecycle work is in progress. They announce busy/disabled semantics and ignore repeat activation until the operation settles, so async work cannot drop the keyboard ring. @@ -2139,6 +2153,19 @@ throws, that source reports failure without a contradictory success entry. The existing per-record append continues to support large feeds; sequential fetching, trailing-24-hour filtering and partial-success caching are unchanged. +FIRMS requests retry once over IPv4 when every resolved address fails to +connect. Node's network-family auto-selection gives each address a short +attempt timeout, so on hosts where IPv6 is unreachable a slow IPv4 handshake +was abandoned and the request reported only `fetch failed` (#68). The retry +makes a single IPv4 connection within the same deadline and the same body +cap, and does not follow redirects. HTTP errors, an expired request deadline +and DNS failures are not retried; a per-address connect timeout, which Node 26 +reports as `ConnectTimeoutError`, is. A retry answered with a null-body status +(204, 205, 304) or a status above 599 fails that source instead of crashing +the server, and a non-OK response releases its connection at once. FIRMS +warnings include the per-address connection errors and never the request URL, +which embeds the MAP_KEY. + ## Installations and map-source guidance - Keyless mapped installations use OpenFreeMap `landuse` military polygons, @@ -2287,8 +2314,9 @@ fetching, trailing-24-hour filtering and partial-success caching are unchanged. > to yield to: whoever handles ESC first calls `preventDefault()` **and** > `stopImmediatePropagation()`, and the launcher skips `defaultPrevented` events. > `stopPropagation()` alone does **not** stop later listeners on the same -> `document` — that is exactly how the compact Radio disclosure made one key -> close the disclosure _and_ dismiss the launcher. +> `document` — that is exactly how a disclosure that called it alone made one +> key close the disclosure _and_ dismiss the launcher. Cockpit's utility +> disclosures (Display, Radio) claim the key the immediate way. > > **Accepted:** a surface class that never clears means no launcher for that page > load, with no timeout. None of the four classes is restored at startup, so an @@ -2968,7 +2996,7 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with > corridor). Its minimal Display popover exposes Intel HUD, > Detection, Parameters, and 3D aircraft. During Cockpit, those existing > standard Display controls move into the Cockpit popover and retain their normal -> nested interaction: HUD plus Tactical/Operator/Minimal layout, Detection plus +> nested interaction: HUD plus Tactical/Minimal/Cyber layout, Detection plus > Density/Allocation/Fade/Outside tuning, and 3D plus Proximity/All mode. The > same nodes and state return to the map Display on exit, so Cockpit does not > maintain a second control state. **Detection is owned by the CONTACTS @@ -3030,15 +3058,10 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with > stacking. Narrow mobile Cockpit > viewports suppress the legacy layer stack, peripheral HUD, and secondary > Context/briefing panes so the flight instruments and primary controls remain -> unobstructed. Outside Cockpit, Radio starts collapsed while off. When Context -> is collapsed, its Radio header icon opens compact controls whose explicit -> Enable/Disable action owns power; stable close and full-panel buttons own -> compact dismissal and one-way detailed expansion independently. When Context -> is already expanded, that same icon skips the floating compact card, expands -> the embedded Radio section, scrolls it into view, and moves keyboard focus to -> its disclosure control. The detailed panel's normal accordion control owns -> its collapse. Compact/detailed state and playback continuity remain shared. -> Radio volume in the full, compact, and Cockpit surfaces, plus Space Mission +> unobstructed. Outside Cockpit, Radio starts collapsed while off and lives +> only in its section inside Context; the detailed panel's normal accordion +> control owns its disclosure and its Enable/Disable action owns power. +> Radio volume in the full and Cockpit surfaces, plus Space Mission > replay speed, use Display / Sharpen's muted 3px rail, circular cyan thumb, > glow, and mono value treatment. Their larger transparent hit areas, keyboard > focus indication, ranges, disabled states, and mission speed scale remain @@ -3823,7 +3846,7 @@ test:track` 43 tracking invariants · headless QA harnesses under ### Interface localization (October 2026) -Shipped locales: **en** (source and fallback), **es**, **it**, **pt** (Brazilian Portuguese), **ru**, **uk**, **zh-CN** and **zh-Hant**. The code lives in +Shipped locales: **en** (source and fallback), **de**, **es**, **fr**, **it**, **ja**, **pt** (Brazilian Portuguese), **ru**, **uk**, **zh-CN** and **zh-Hant**. The code lives in `src/i18n/`. How to mark UI text: [`docs/I18N.md`](./I18N.md); translator workflow: [`docs/TRANSLATORS.md`](./TRANSLATORS.md). @@ -3901,7 +3924,7 @@ its criteria cannot be silently ignored. | Satellites | CelesTrak | `src/data/satellites.js` | `/api/celestrak` | 120s | | Space Missions (30d) | Launch Library 2 + CelesTrak | `src/data/rocketLaunches.js` | `/api/launches` + `/api/celestrak/active` | 5 min | | Traffic | Selectable TomTom / OpenStreetMap / Hybrid roads; optional TomTom flow (BYOK) | `src/data/traffic.js` | browser-direct OpenFreeMap tiles; `/api/tomtom` for flow | viewport-driven; capped tile cache | -| CCTV | Austin + Caltrans (CA) + TfL London + Ontario 511 + Fintraffic (FI) + DriveBC (BC) + TxDOT (TX) + Estonia (Tallinn, Tarktee) + Live Traffic NSW + Open Calgary Open Data + Statens vegvesen (NO, stills + live HLS, Kartverket ground heights) + King County (WA) Open Data + WSDOT (WA) Open Data + NYC DOT TMC (opt-in) + City of Toronto RESCU + Catalonia (Servei Català de Trànsit) + Hong Kong Transport Department + FL511 (FL, opt-in) + Freeway Bureau, MOTC (Taiwan) + Street View fallback | `src/data/cctv.js` | `/api/cctv` | 10s (active) | +| CCTV | Austin + Caltrans (CA) + TfL London + Ontario 511 + Fintraffic (FI) + DriveBC (BC) + TxDOT (TX) + Estonia (Tallinn, Tarktee) + Live Traffic NSW + Open Calgary Open Data + Statens vegvesen (NO, stills + live HLS, Kartverket ground heights) + King County (WA) Open Data + WSDOT (WA) Open Data + NYC DOT TMC (opt-in) + City of Toronto RESCU + Catalonia (Servei Català de Trànsit) + Hong Kong Transport Department + FL511 (FL, opt-in) + Freeway Bureau, MOTC (Taiwan) + Autostrada del Brennero (IT, A22 stills) + Street View fallback | `src/data/cctv.js` | `/api/cctv` | 10s (active) | | Radio | Radio Browser (public-domain station directory) | `src/data/radio.js` | `/api/radio/stations`, `/api/radio/click/:uuid` | 45 min directory refresh | | Transit 🚌 | Operator GTFS-Realtime VehiclePositions (7 keyless regions, `src/data/transitFeeds.js`) | `src/layers/transit/` via `src/app/layers/transit.js` | `/api/transit` | 15s (poll + delayed playback) | | Bikeshare 🚲 | GBFS (Lyft + BCycle + Social Bicycles + PBSC incl. Bike Share Toronto, Bike Itaú (Brazil), Bicing (Barcelona) and seven other Spanish cities + Urban Sharing, Entur, JCDecaux (Lillestrøm, Valencia, Seville) + nextbike (AMBici)) | `src/data/bikeshare.js` | `/api/gbfs` | 60s | @@ -4814,13 +4837,6 @@ synchronous playback-control observers run, so reentrant voice cleanup cannot overwrite a released stream with a stale paused state. Layer destruction likewise retains an enabled manager entry when module disable or destroy fails semantically, preventing an active orphan and allowing retry. -When Context is collapsed, explicitly activating its Radio header icon reveals -the compact transport; hover and focus alone do not open it. When Context is -expanded, the icon instead expands and scrolls to the embedded Radio section. -Its accessible label, controlled region, and expanded state keep describing the -current route throughout enabling, enabled, disabling, and uncertain lifecycle -renders. Both routes preserve Radio power, playback, station, filter, and volume -state. Bundled datasets live in `src/data/local_data/` with per-folder provenance READMEs; they are lazily loaded via `src/data/localGeojsonCore.js` (Vite `?url` assets) when toggled on. @@ -4841,6 +4857,8 @@ silently demoting every later lookup for the session. - The expanded `CONTEXT` view offers mutually exclusive `CONTACTS` and `SPACE MISSIONS` modes. Selecting `CONTACTS` enables the context-owned Flights, Military Flights, AIS Vessels, and Mapped Installations dependencies only when they are not already user-enabled; selecting `SPACE MISSIONS` enables the recent-launch layer and its Satellite dependency. `CONTACTS` cycles the nearest supported contact of whatever type is selected. Satellites are deliberately excluded from those Awareness cohorts and keep their own tracking UX. Selecting the active mode again returns to the neutral chooser and releases only mode-owned dependencies. - If a civilian or military aircraft is already tracked when `CONTACTS` becomes operational, that source-owned track is adopted as the Context subject before nearest-contact autofocus. Context rechecks the tracker after its dependencies settle, so a newer selection wins, while an explicit clear during activation prevents fallback from silently selecting a replacement. Cockpit entry remains unavailable until that Context transaction has settled, so its camera takeover cannot clear Cesium tracking before adoption. Adoption does not recreate tracking or transfer camera ownership; it initializes the normal 250 km ring, history, proximity results, and Cockpit Previous/Next state for the original aircraft. - `SEARCH NEARBY SITES` retains the bounded OSM results and makes one user-initiated, view-biased Google Maps Places text search for “military installation.” Google results are source-stamped, deduplicated against OSM by rounded location/name, and remain mapped context rather than operational claims. If Places is unavailable or the API is not enabled for the supplied key, OSM context remains available. A view across the antimeridian is searched around its own centre, with a radius from its real width. +- The Context header holds only its title and collapse control. It no longer carries a Radio shortcut or a compact Radio player: Radio is the `RADIO` section inside the panel, with its own disclosure and the single `ENABLE` power control. Cockpit keeps its own compact Radio utility. +- Context is set for reading in every theme: mode, standby and action labels are 10 px, and the panel draws no glow halo. Outside Cyber, the two modes form one segmented control, the standby guidance is plain left-aligned text rather than a dashed placeholder, and the panel's section titles share the side panels' dock-label colour. Cyber keeps its separately framed mode buttons, its red panel titles, and in layout-focus a framed, centred standby card over its radar art. - The expanded desktop header omits the redundant `ON` label; the active mode button carries state. Expanded Contact results also omit the duplicate `GLOBAL CONTEXT` / `CONTEXT ONLY` status row and begin with the selected subject and its 250 km scope. Global Context does not fabricate a selected-entity model preview: the provisional hand-authored aircraft wireframe was removed because it was not geometry extracted from the selected entity's actual asset. - Dependency ownership is reversible: disabling Global Context releases only dependencies it enabled, while user-enabled layers remain on. This also removes the Military-layer suppression handoff when Global Context owned Military, allowing an already-enabled civilian Flights layer to resume its normal mixed rendering. If OpenSky is unavailable and has no last-good cache, Flights requests a capped 250 nm adsb.lol point snapshot around the current view anchor and labels that provenance explicitly; it never relabels military-feed rows as civilian data. If both inputs fail, Flights remains `UNKNOWN`. - Space Missions is replay-isolated: Rocket Launches and Satellites are the only Data Layers permitted while the mode is active. Direct UI and voice entry capture the same pre-entry snapshot; internal dependency and restoration enables do not create a user-owned Context session. Entry waits for incompatible layers to shut down, direct incompatible enables are blocked before lifecycle work, and the entry gate remains active through the complete Rocket Launches enable. A newer same-target ON request takes ownership of the pending entry without releasing its isolation snapshot, including when it arrives while the prior request is awaiting the adoption guard. A caller abort, resource cancellation, newer OFF, or layer teardown waits for exact manager settlement and restores that snapshot without resurrecting Rocket Launches. If an abort lands after only part of a restore settles, Context completes the same exact target without the stale caller signal and then replays newer explicit layer intent. Dedicated Voice Context cancellation reports a stable cancelled result plus the current Context state; generic layer visibility additionally exposes manager phase, reason, successor, and lifecycle details. Once an exact voice visibility intent commits, a newer voice turn cannot relabel it as cancelled while Context settlement completes; pre-commit aborts remain cancellable and final lifecycle mismatches remain failures. Clear Selected Layers reserves its complete captured OFF set before sequential teardown; a newer absolute request of any origin supersedes only its layer reservation and remains authoritative. A rejected layer teardown retains the truthful enabled state, rolls already-stopped siblings back to the captured pre-entry set, and aborts replay; rapid exit/re-entry serializes the full Satellite enabled-state and parameter restore before a new snapshot is taken. Contacts remains additive and restores user-enabled layers normally. @@ -5631,7 +5649,7 @@ apply to `public/` assets or built-preview serving. - The desktop right rail (`#right-context-rail`) owns `DISPLAY`, `CCTV`, its active parameter controls, and `GLOBAL CONTEXT` as one fixed responsive stack in that order. Its compact buttons use the same 176 px width as the left accordion and one consistent 50 px height, share the left stack's 52 px edge inset and measured top baseline across HUD variants, then constrain themselves against visible HUD/chrome rectangles and the remaining vertical corridor. `DISPLAY` is no longer draggable and legacy saved coordinates are ignored. - The right rail is labeled **DISPLAY** (formerly "MOVE") and groups, in order, HUD, DETECT, Bloom, Sharpen, 3D, Clean-UI (HUD + DETECT promoted to the top). Its expanded controls retain the same compact 176 px width as the right-side tabs instead of growing to the wider Context detail-card width. It starts expanded on first run and respects the user's later `v6` collapse choice. Collapses/expands with directional chevrons (`◀` collapsed, `▶` expanded). - Display and Context use matching 330 px expanded widths and matching compact tab dimensions. The parameter panel is part of Display's expanded content. DISPLAY may remain open beside one contextual panel; CCTV and Context are mutually exclusive. In Tactical HUD, expanding CCTV or Context hides the other contextual launcher while DISPLAY remains independently available. The most recently opened right-rail panel owns the constrained lane even when it appears later in DOM order; passive restoration and automatic disclosure do not replace that explicit owner. Minimal and other HUD layouts retain the collapsed launchers; when their active panel exceeds the measured corridor, the rail reserves sibling heights and gaps and scrolls the active panel internally. -- At 720px and below both panel stacks (`#left-panel-stack`, `#right-context-rail`) go edge-to-edge and scroll vertically. Every hosted panel's decorative `.panel-glow` is pinned to its panel box there (`inset: 0` in `src/ui/styles/responsive.css`): on desktop the glow overhangs the panel by 18–20 px as harmless paint, but inside a scroll container that overhang is scrollable overflow on both axes and drew a horizontal scrollbar band plus a vertical scrollbar that scrolled nothing but glow whenever CCTV, Context, Data Layers or Scenes was expanded. The glow's blur is ink overflow and never scrolls, so the look is unchanged. Do not fix this by clipping the stacks: `overflow-x: clip` would also clip the Context radio popover and would not remove the vertical scrollbar. Pinned by `src/ui/panelRails.test.mjs` against every `(max-width: 720px)` block in the flattened stylesheet. +- At 720px and below both panel stacks (`#left-panel-stack`, `#right-context-rail`) go edge-to-edge and scroll vertically. Every hosted panel's decorative `.panel-glow` is pinned to its panel box there (`inset: 0` in `src/ui/styles/responsive.css`): on desktop the glow overhangs the panel by 18–20 px as harmless paint, but inside a scroll container that overhang is scrollable overflow on both axes and drew a horizontal scrollbar band plus a vertical scrollbar that scrolled nothing but glow whenever CCTV, Context, Data Layers or Scenes was expanded. The glow's blur is ink overflow and never scrolls, so the look is unchanged. Do not fix this by clipping the stacks: `overflow-x: clip` would not remove the vertical scrollbar. Context draws no glow at all (`display: none`), so it is not part of that pin. Pinned by `src/ui/panelRails.test.mjs` against every `(max-width: 720px)` block in the flattened stylesheet. - `STYLE PRESETS` and `LOCATIONS` start collapsed, expand on intentional hover/click, and auto-collapse after hover leave delay. - Collapsed mini-status indicators show active style and active location/landmark. - Detection mode is user-controlled and should persist when switching styles. Since 2026-08-22 it also STARTS on — Dense @ 75% for every style on a first run, Normal included — as a `GLOBAL_POST_DEFAULTS` baseline that does NOT set `_detectionUserOverridden`. Exception (unchanged): selecting a military style (CRT/NVG/FLIR) auto-enables the same Dense preset, but only until the user manually changes detection this session (`_detectionUserOverridden` gate), after which style switches never touch it. diff --git a/docs/SHARE-LINK-FORMAT.md b/docs/SHARE-LINK-FORMAT.md index ea5636f39..5728d1ec3 100644 --- a/docs/SHARE-LINK-FORMAT.md +++ b/docs/SHARE-LINK-FORMAT.md @@ -107,7 +107,7 @@ probability of about 255 in 256 (many of them already fail as base64url). | 13 | thing | LEN | none | sub-message, see thing views | | 14 | `style` | enum | `normal` | normal, retro, surveillance, thermal, anime, noir, snow (retired) | | 15 | `mapStack` | enum | `photoreal` | photoreal, bing-aerial, bing-labels, esri-imagery, osm | -| 16 | `hudVariant` | enum | style | tactical, operator, minimal, cyber | +| 16 | `hudVariant` | enum | style | tactical, operator (retired), minimal, cyber | | 17 | `bloomIntensity` | uint | style | | | 18 | `bloomVersion` | uint | 2 | | | 19 | `sharpenIntensity` | uint | style | | @@ -129,6 +129,9 @@ table learns its code. A new style's settings are encoded against its own defaults, while a decoder that does not know the style uses Normal's; adding a style also extends the pinned style-defaults table. +`hudVariant` code 1 (`operator`) is retired: the app no longer offers that +layout and restores it as `tactical`. The code stays reserved. + Flag bits (field 9): 0 `bloom`, 1 `sharpen`, 2 `hudVisible`, 3 `celestialRing` (each defaulting by style), 4 `scopeEnabled` (default on). diff --git a/docs/TRANSLATORS.md b/docs/TRANSLATORS.md index 9253d094f..f862d8e4c 100644 --- a/docs/TRANSLATORS.md +++ b/docs/TRANSLATORS.md @@ -1,6 +1,6 @@ # Translating God's Eye View -Shipped languages: **English** (source and fallback), **Spanish** (`es`), **Italian** (`it`), **Brazilian Portuguese** (`pt`), **Russian** (`ru`), **Ukrainian** (`uk`), **Simplified Chinese** (`zh-CN`) and **Traditional Chinese** (`zh-Hant`). +Shipped languages: **English** (source and fallback), **German** (`de`), **Spanish** (`es`), **French** (`fr`), **Italian** (`it`), **Japanese** (`ja`), **Brazilian Portuguese** (`pt`), **Russian** (`ru`), **Ukrainian** (`uk`), **Simplified Chinese** (`zh-CN`) and **Traditional Chinese** (`zh-Hant`). ## How it works @@ -63,4 +63,4 @@ These are contracts, not copy: layer ids and registry keys, status enums and oth ## Language notes -Each language keeps its glossary, plural forms, units and style in its own file, `docs/translations/.md`: [Spanish](./translations/es.md), [Italian](./translations/it.md), [Brazilian Portuguese](./translations/pt.md), [Russian](./translations/ru.md), [Ukrainian](./translations/uk.md), [Simplified Chinese](./translations/zh-CN.md), [Traditional Chinese](./translations/zh-Hant.md). Read it before translating, and add a term there when you settle on one. +Each language keeps its glossary, plural forms, units and style in its own file, `docs/translations/.md`: [German](./translations/de.md), [Spanish](./translations/es.md), [French](./translations/fr.md), [Italian](./translations/it.md), [Japanese](./translations/ja.md), [Brazilian Portuguese](./translations/pt.md), [Russian](./translations/ru.md), [Ukrainian](./translations/uk.md), [Simplified Chinese](./translations/zh-CN.md), [Traditional Chinese](./translations/zh-Hant.md). Read it before translating, and add a term there when you settle on one. diff --git a/docs/translations/de.md b/docs/translations/de.md new file mode 100644 index 000000000..86032a238 --- /dev/null +++ b/docs/translations/de.md @@ -0,0 +1,108 @@ +# German (`de`) + +Catalog: `src/i18n/locales/de.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md). + +Translation source: abiyeenzo (#908). + +## Style + +- Standard German that reads naturally in Germany, Austria and Switzerland; ß where it belongs (Straße, schließen), no Swiss ss. +- No direct address: neither «Sie» nor «du». Actions and hints use the neutral infinitive: «Erneut versuchen», «Kamera anklicken zum Aktivieren», «Zuerst eine Aufnahme wählen», «Mikrofonzugriff erlauben und erneut versuchen». "Your" becomes the article or «eigene»: «Eigener RTL-SDR-Empfänger», «Die IP-Adresse ist für diesen Sender sichtbar». +- Nouns are capitalized, as German requires; otherwise sentence case, not Title Case: "Live Flights" → «Live-Flüge», "Provider Settings" → «Anbieter-Einstellungen». UPPERCASE English stays UPPERCASE: «LIVE-DATEN AUS», «KEINE ENTWARNUNG». In UPPERCASE, ß is written SS: «SCHLIESSEN», «STRASSENEBENE». +- Compounds are written as one word or with a hyphen next to a name, code or English term: «Datenebene», «Wetterkacheln», «TomTom-Schlüssel», «AIS-Schiffe», «Street-View-Fallback». Do not split them with spaces. +- Short labels, chips and HUD readouts stay short. German runs about a third longer than English, so prefer a short word or a standard abbreviation with a period over a wrapped line: REICHW., GESCHW., NIEDERSCHL., SONST., NACHR., VERST., VGL., KAL., KOMM. (a bare «KOMM» reads as the imperative) +- The avionics codes HDG, BRG, CRS, DEST, SPD, ALT, GS, TRK, V/S, FOV and SQUAWK stay in English, as in the cockpit. Words used in a sentence are translated: heading → «Kurs», bearing → «Peilung», range → «Reichweite». +- A dash between clauses is a spaced en dash « – »; a colon is fine where it explains: «Feed ausgefallen – neue Versuche in größeren Abständen», «SIMULIERT: Verkehrsdienst nicht erreichbar». Quotes are „…“. +- Status and waiting messages use the neutral infinitive too: «Warten auf Aktualisierung», not «Warte auf …». Reasons inserted after a dash stay lower case: «Szene nicht gespeichert – das Projekt ist größer als 5 MiB». +- A layer named in a sentence takes its panel name in quotes: «Die Ebene „Live-Flüge“ ist nicht aktiviert», «die Ebene „Live-Flüge“ lädt Daten für den jeweiligen Bildausschnitt». +- Rolling time windows say their length, so they cannot read as the previous calendar period: LAST YEAR → «LETZTE 12 MONATE», LAST MONTH → «LETZTE 30 TAGE». +- Keyboard keys are named as on German keyboards where they differ: Leertaste, Rücktaste, Umschalt; Esc, Enter, Tab and letter keys stay. +- Decimal comma in fixed text: «M4,4», «28,2710° N». Thousands separator is a period: «1.000 km». Formatted numbers come from `Intl` and are already German. +- Units: km, m, km/h, m/s, kn (knots, also in the cockpit), ft (feet), hPa, MW, h, min, s, T (days, in short readouts); in UPPERCASE text KM, KM/H, KN, FT, MIN, H, T («VOR {count} H»). A space between number and unit, in compact readouts too: «{value} m», «{count} min», «50 %», «{speed} KN», «{count} / 30 T». `°` sticks to the number. +- "ago" goes before the time: «vor {count} min», «vor {value}». When a placeholder already holds a localized "ago" text (`{age}` in `STALE · cached {age}`, `{ago}`), do not add «vor» again. +- Compass points use O for Ost: N, NNO, NO, ONO, O, OSO, SO, SSO, S, SSW, SW, WSW, W, WNW, NW, NNW; hemisphere letters N, S, O, W. +- Routing: direction words are lowercase adverbs (links, leicht rechts, scharf links, geradeaus), so a sentence never starts with one: «Auf {road} {direction} abbiegen», «Hier {direction} abbiegen», «Weiter {direction}». «Hier», not «Dann»: the step card already says «dann {leg}» and «Nächster Schritt: …». At a fork the verb is «weiter», which also works with «geradeaus»: «An der Gabelung {direction} weiter». Roundabout exits are ordinals with a period: «die {exit} Ausfahrt» → «die 2. Ausfahrt». A lane is named by where it leads, not by its position: «Die geradeaus führende Spur benutzen», «die leicht nach links führende», «die scharf nach rechts führende» (not «halblinke» or «äußerst linke»). + +## Plurals + +A `tn()` entry needs two forms. `Intl.PluralRules('de')` picks: + +| Form | Counts | Example | +| ------- | --------------------------- | --------- | +| `one` | 1 | 1 Kamera | +| `other` | everything else: 0, 2, 1,5… | 3 Kameras | + +Keep the verb in agreement: «{count} Station meldet» / «{count} Stationen melden». Abbreviated units and codes do not change: «{count} NACHR.», «{count} EMPFANGEN». + +## Glossary + +| English | Deutsch | +| ---------------------------------------------- | ---------------------------------------------------------------------- | +| layer / data layer | Ebene / Datenebene | +| feed / source / data source / input (cockpit) | Feed / Quelle / Datenquelle / Quelle | +| LIVE (status) / live data | LIVE / Live-Daten | +| SIMULATED | SIMULIERT | +| stale / degraded / unavailable | veraltet / eingeschränkt / nicht verfügbar | +| retry / retrying / retrying slowly | neuer Versuch / erneut versuchen / neue Versuche in größeren Abständen | +| fallback | Fallback | +| key (API key) / POWER UP / POWERED UP | Schlüssel / STARTKLAR / HOCHGEFAHREN | +| Provider Settings | Anbieter-Einstellungen | +| ON / OFF (state chips) | EIN / AUS | +| enable / disable | aktivieren / deaktivieren | +| globe / map / map source / map stack / basemap | Globus / Karte / Kartenquelle / Kartenstapel / Basiskarte | +| aircraft / flight / military flights | Luftfahrzeug (HUD and short labels: Flugzeug) / Flug / Militärflüge | +| vessel, ship | Schiff | +| satellite / shell (Starlink) / DENSE | Satellit / Schale / DICHT | +| launch (rocket) / launch site / space mission | Start / Startplatz / Raummission | +| ascent / replay / payload / stage / downrange | Aufstieg / Replay, Wiedergabe / Nutzlast / Stufe / Downrange | +| contact / tracking | Kontakt / Verfolgung | +| installation / site | Anlage / Standort | +| mapped / observed | kartiert / beobachtet (Bhote chip OBSERVED IMAGERY: REALE BILDDATEN) | +| camera / CCTV camera / camera badge | Kamera / CCTV-Kamera / Kamera-Markierung | +| earthquake / active fire / fire perimeter | Erdbeben / aktiver Brand / Brandgrenze | +| cyclone / advisory (NHC/CPHC) / cone | Wirbelsturm / Lagebericht (not «Warnung») / Kegel | +| issued (advisory, forecast) | ausgegeben | +| radio station | Sender | +| broadcast FM | UKW | +| road / flow (traffic) / flow coverage | Straße / Verkehrsfluss / Live-Abdeckung | +| transit / bike share | ÖPNV / Bikesharing | +| scene / shot / scene recipe | Szene / Aufnahme / Szenenrezept | +| capture shot / camera move | Aufnahme speichern / Kameraflug | +| anchor / data pack / bundle | Anker / Datenpaket / Bundle | +| story beat / evidence beat | Story-Schritt / Beweisschritt | +| briefing / panel / dock | Briefing / Bereich / Dock | +| cockpit | Cockpit | +| preset / visual preset | Preset / visuelles Preset | +| Scope (circular viewport mask) / keyhole | Scope / Schlüsselloch | +| feather / fade | Weichzeichnung / Ausblenden | +| overlay / detection overlay | Overlay / Erkennungs-Overlay | +| Clean UI | Reine Ansicht | +| Night vision / Thermal / Snow (cockpit styles) | Nachtsicht / Wärmebild / Schnee | +| heading / altitude / ground speed | Kurs / Höhe / Geschwindigkeit über Grund (short: GESCHW. ÜBER GRUND) | +| HDG / BRG / CRS / DEST / SPD / FOV | unchanged | +| RANGE / PITCH / HGT (CCTV calibration chips) | REICHW. / NEIGUNG / HÖHE | +| PREV / NEXT | ZURÜCK / WEITER | +| RESUME (radio transport button) | FORTSETZEN (not «WEITER», which is NEXT in the same row) | +| nearest (spatial) | nächstgelegen (always; «nächste» is NEXT) | +| expand / collapse (sections) | ausklappen / einklappen | +| L / R (relative bearing) | L / R | +| SIG / NEWS / LOCAL (briefing tabs) | SIG / NEWS / LOKAL | +| NOT AN ALL-CLEAR | KEINE ENTWARNUNG | +| Street Level / Street View (Google) | Straßenebene / Street View | +| Recent Imagery / box / pin (a day) | Aktuelle Bilddaten / Bereich / fixieren | +| frame (bring into view) | ins Bild nehmen | +| mark (map annotation) / outline | Markierung / Umriss | +| voice (control) / voice session | Sprachsteuerung / Sprachsitzung | +| YOU / THIS / HERE (voice card labels) | EINGABE / DIES / HIER | +| click / click to … | anklicken / … klicken | +| tap (touch) | Antippen (not «Tipp», which is a hint) | +| press / tap a key | Taste drücken / kurz drücken (never «tippen» for keys) | +| tilted view | Schrägansicht | +| card (host card) / voice card | Eintrag (in hints), Infokarte / Sprachkarte | +| label (on the map) / clear (switch off layers) | Beschriftung / ausschalten (not «löschen») | +| station tag (radio filter) | Kategorie | +| DIRECTORY BAND / {category} BAND (radio dial) | SENDERSKALA / SENDERSKALA · {category} (not «-BAND»: «ROCK-BAND») | +| MIC (dock button) | MIKRO | +| frame (weather, video) / image | Frame / Bild | +| collapse / landslide (Bhote Koshi) | Erdrutsch (not «Abbruch», which reads as "cancel") | +| website (radio station) | Website (SENDER-WEBSITE; «Standort» is a physical site) | diff --git a/docs/translations/fr.md b/docs/translations/fr.md new file mode 100644 index 000000000..e2f506988 --- /dev/null +++ b/docs/translations/fr.md @@ -0,0 +1,105 @@ +# French (`fr`) + +Catalog: `src/i18n/locales/fr.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md). + +Translation source: abiyeenzo (#907); the French part of meirm's #312 was used for comparison. + +## Style + +- Standard French that reads naturally in France, Belgium, Switzerland and Canada; avoid regionalisms. +- Address the user with "vous" imperatives: « Cliquez », « Activez », « Réessayez ». Tooltips that name an action use the infinitive: « Réduire le panneau », « Réinitialiser la caméra ». UPPERCASE HUD instructions also use the infinitive: « CLIQUER SUR UNE CAMÉRA POUR L'ACTIVER ». +- Sentence case, not Title Case: "Live Flights" → « Vols en direct ». UPPERCASE English stays UPPERCASE, accents included: « ÉTAT », « PRÉC. ». +- Typography: a narrow no-break space (U+202F) goes before `:` `;` `?` `!` and `%`, and inside « » quotes: `Supprimer le plan « {shot} » ?`. It keeps the sign on the same line as the word before it. Do not type a plain space there. The same narrow no-break space separates thousands in a number written into the text: « 1 000 km ». Ellipsis is `…`, not `...`. Dashes keep their spaces: « flux indisponible — réessayez ». +- Short labels, chips and HUD readouts stay short. A standard abbreviation with a period beats a wrapped line: PRÉC., SUIV., RÉINIT., ENREG., INDISP., DIST. +- The HUD codes HDG, BRG, CRS, FOV, DEST, SPD and ALT stay in English everywhere, as in cockpit avionics. Words are translated: RANGE → « PORTÉE », heading in a sentence → « cap », ground-track course → « route » (« ROUTE 270° »). +- ON and OFF stay as they are on HUD toggles and chips (« CCTV ON », « PROJECTION OFF »); in a sentence use « activé » / « désactivé ». +- Keyboard keys stay in English: Space, Esc, Enter, Tab, Shift, Backspace, H, 1–9 (« Maintenez Space pour parler », « ESC pour fermer »). +- Units: km, m, km/h, m/s, nd (knots), ft (feet), h, min, s, with a space before the unit: « 250 km », « il y a 5 min »; in UPPERCASE text KM, KM/S, ND, FT, MIN. MW, dBZ, hPa and `°` stay. Decimal comma in fixed numbers: « 28,2710° N », « M4,4 ». Money: « 12 k$ », « 3 M$ », « 2 Md$ ». +- "ago" goes before the time: « il y a {count} min ». When a placeholder already holds a localized "ago" text (`{age}`, `{ago}`), do not add « il y a » again. +- Round-about exit ordinals: 1re, 2e, 3e… (« prenez la 1re sortie »). + +## Plurals + +A `tn()` entry needs three forms. `Intl.PluralRules('fr')` picks: + +| Form | Counts | Example | +| ------- | -------------------------------------- | -------------------- | +| `one` | 0 and 1, fractions below 2 (1,5) | 1 caméra, 0 caméra | +| `many` | 1 000 000, 2 000 000… (exact millions) | 1 000 000 de caméras | +| `other` | everything else: 2–999 999, 2,5… | 3 caméras | + +The `many` form is the `other` text with « de » (« d' » before a vowel) after the number: « {count} de couches effacées », « {count} D'ENTRÉES INCONNUES ». When the words after the number cannot agree (an UPPERCASE readout, a list), put the number after a colon in every form: « CAPTÉS : {count} ». The same goes for a layer name whose gender and number vary: « Couche activée : {label} », not « {label} activé », or a tool name: « Outil actif : {name} », not « {name} est actif ». + +## Glossary + +| English | Français | +| ------------------------------------------------------------ | ----------------------------------------------------------------------- | +| layer / data layer / DATA LAYERS (panel title) | couche / couche de données / COUCHES | +| feed / source / data source | flux / source / source de données | +| LIVE (status) / live data | DIRECT / données en direct | +| SIMULATED | SIMULÉ | +| stale / degraded / unavailable | obsolète / dégradé / indisponible | +| retry / retrying (status: RETRYING X) | nouvel essai / réessayez (NOUVEL ESSAI : X) | +| rate-limited | limite de requêtes atteinte | +| key (API key) / POWER UP / POWERED UP | clé (clé API) / ALLUMER / ALLUMÉ | +| Provider Settings | paramètres des fournisseurs | +| enable / disable | activer / désactiver | +| globe / map / map source / basemap / map stack | globe / carte / source de carte / fond de carte / fond de carte | +| tile (map) | tuile | +| aircraft / flight / military flights | aéronef (avion in short copy) / vol / vols militaires | +| vessel, ship / tanker (AIS type) | navire / navire-citerne | +| satellite / shell (Starlink) / DENSE | satellite / couche de la constellation / DENSE | +| launch / launch site / space mission | lancement / site de lancement / mission spatiale | +| ascent / replay / payload / stage / downrange | ascension / rejeu / charge utile / étage / distance au sol (DIST. SOL) | +| contact / tracking / track (HUD) | contact / suivi / piste | +| brackets (HUD corner brackets around contacts) | cadres de ciblage | +| installation / site | installation / site | +| mapped / observed | cartographié / observé | +| camera / CCTV camera / ALPR camera | caméra / caméra CCTV / caméra LAPI | +| earthquake | séisme | +| active fire (FIRMS detection) / wildfire | feu actif / incendie | +| contained / containment (wildfire) | circonscrit / part circonscrite (not « maîtrisé », which is controlled) | +| advisory (NHC/CPHC cyclone) | bulletin | +| forecast / lead hours / valid / issued | prévision / échéances / valide / émise | +| AQHI (Canada) | CQAS (cote air santé) | +| radio station / tune | station / régler | +| bikeshare / transit | vélos partagés / transports en commun | +| route (directions) / turn (maneuver) | itinéraire / manœuvre | +| scene / shot / scene recipe | scène / plan / recette de scène | +| bundle / data pack / anchor | archive / paquet de données / ancre | +| import / export (noun: file, scene, project) | importation / exportation (not « import » / « export ») | +| capture camera as anchor / set move start (scene editor) | ancre à la position de la caméra / vue actuelle comme départ | +| story beat | étape du récit | +| briefing / panel / dock | briefing / panneau / dock | +| cockpit | cockpit | +| preset / visual preset | préréglage / préréglage visuel | +| Scope (circular viewport mask) / keyhole | lunette / ouverture | +| detection overlay | surcouche de détection | +| Clean UI | vue épurée | +| Street Level (layer) / Street View (Google) | Niveau rue / Street View | +| Recent Imagery / box | imagerie récente / zone | +| Daily overview / overview (Recent Imagery VIIRS product) | vue quotidienne (one name for the product, not « vue d'ensemble ») | +| pin (Draw tool) / pin (Recent Imagery) | épingle / épingler | +| Cyber (HUD layout, effect) | Cyber | +| heading / course (ground track) / altitude / ground speed | cap / route / altitude / vitesse sol | +| HDG / BRG / CRS / FOV / DEST / SPD / ALT | unchanged | +| RANGE / PITCH / HGT (CCTV calibration chips) | PORTÉE / INCL. / HAUT. | +| PREV / PREVIOUS / NEXT (chips) | PRÉC. / PRÉC. / SUIV. | +| Previous / Next (button text) | Précédent / Suivant | +| L / R (relative bearing) | G / D | +| SIG / NEWS / LOCAL (briefing tabs) | SIG / ACTUS / LOCAL | +| compass points (N, NNE, …, W, NW) | N, NNE, …, O, NO | +| NOT AN ALL-CLEAR | PAS UN FEU VERT | +| AI agent | agent IA | +| share (a view or link) | partager | +| click / click to … | cliquez / cliquer pour … | +| map heading (compass, north-up control) | orientation (de la carte) | +| subject (cockpit / shared contact) | cible | +| cloud cover / CLOUD (briefing readout) | nébulosité / NÉBULOSITÉ | +| LEVEL (cockpit horizon guide) | HORIZON | +| CALIBRATED / CURATED / RAW PRIOR (CCTV pose) | CALIBRÉE / VÉRIFIÉE / ESTIM. INITIALE | +| DATASHEET / SPEC FOV (CCTV lens range) | FICHE TECHNIQUE / FOV CONSTRUCTEUR | +| PORT TENDER (AIS type) | SERVITUDE PORT. (navire de servitude portuaire) | +| ChatGPT sign-in (action) / opening sign-in / sign-in expired | connexion / page de connexion / la session a expiré | +| authored shot (scene) | plan scénarisé | +| report / record (USGS event card) / readout, reading | rapport / relevé (a measured value only) | diff --git a/docs/translations/it.md b/docs/translations/it.md index 81854d6fe..722916101 100644 --- a/docs/translations/it.md +++ b/docs/translations/it.md @@ -79,7 +79,7 @@ The `many` form is the `other` text with «di» after the number when a noun fol | briefing / panel / dock | briefing / pannello / dock | | cockpit | cockpit | | preset / visual preset | preset / preset visivo | -| Tactical / Operator / Minimal / Cyber | Tattico / Operatore / Minimale / Cyber | +| Tactical / Minimal / Cyber | Tattico / Minimale / Cyber | | Scope (circular viewport mask) / keyhole | scope / foro | | feather / fade | sfumatura / dissolvenza | | detection overlay | overlay di rilevamento | diff --git a/docs/translations/ja.md b/docs/translations/ja.md new file mode 100644 index 000000000..9e612e8e4 --- /dev/null +++ b/docs/translations/ja.md @@ -0,0 +1,112 @@ +# Japanese (`ja`) + +Catalog: `src/i18n/locales/ja.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md). + +Translation source: abiyeenzo (#913). + +## Style + +- Sentences use です/ます: 「キーを保存しました」, 「もう一度お試しください」. Labels, buttons and HUD chips are short nouns or verb stems without a final 。: 「再試行」, 「保存」, 「ライブ」, 「オフ」. +- Japanese has no case: an UPPERCASE English label becomes the same short Japanese label. Codes, units and names stay in their own case (HDG, ADS-B, km). +- Full-width punctuation in Japanese text: 、。:?!()「」. Quote names and titles with 「」: 「{shot}」を削除しますか? Keep `·` as the HUD separator, `…` for an ellipsis and `—`, `→`, `↗` as in English. A dash inside a sentence becomes 。 or :. +- Button names cited in running text go in []: [次へ]を使ってください. +- No space between Japanese and Latin letters, digits or a `{placeholder}`: 「AIS船舶」, 「Spaceを長押しで話す」, 「{count}件」, 「{name}を表示」. +- Two exceptions keep the space: a Japanese label before its value in a readout (「雲量 {pct}%」, 「ステーション {id}」, 「高度 {altitude}」), and the state words in toggles (「CCTV オン」, 「ラジオ オフ」). +- Waiting states are 「X待ち」, not 「Xを待機中」: 「観測データ待ち」, 「進路/予報円は第{number}報待ち」, 「RTL-SDRデバイスの接続待ち…」. 待機中 is for something on standby with no object: 「フィード待機中」, 「音声待機中」. +- Key names keep their own case in running text and hints: Tab, Enter, Space, Esc (「Tabでプレビュー · Enter / Spaceで選択」). Text that CSS upper-cases shows them in capitals, as in English. +- Loanwords are katakana with the long-vowel mark, as in Microsoft and Apple style: ユーザー, ブラウザー, ビューアー, プロバイダー, レイヤー, フィルター, モニター, パラメーター. Pick one katakana form per term and keep it (see the glossary). +- Product, provider and dataset names stay in Latin script: Google Maps, Cesium ion, OpenSky, CelesTrak, NASA FIRMS, Sentinel-2, KartaView. Google's own product names use Google's Japanese, joined like any Latin word: Googleストリートビュー. Nepal place names in the Bhote Koshi scenes stay in Latin script, as in zh-CN; country and well-known city names are translated (ネパール, 東京, オースティン). + +## Numbers, units and dates + +- Units: km, m, km/h, m/s, kt, ft, mph, hPa, MW, dB, °, % stay as written, with the English spacing (「250 km」, 「{frp} MW」, 「95 kt」). Code-built cockpit distances follow this too (`cockpitUnitCase` skips the HUD upper-casing for ja, as for zh). The cockpit's own speed caption is the HUD code KT, in 対地速度 · KT and under the readout. +- Number ranges keep the English en dash, not ~: 「2–6日ごと」, 「50–99%」, 「南緯25°–北緯80°」. +- Time units are words written straight after the number: 秒, 分, 時間, 日 (「5分前」, 「{count}時間」, 「1時間5分」). `formatQuantity` and `formatRelativeTime` drop the space for Japanese unit words, so code-built values match the catalog. +- Counters follow the thing counted: 機 (aircraft), 隻 (ships), 基 (satellites), 台 (cameras, vehicles), 局 (stations), 件 (records, results, events), か所 (sites), 個 (generic). Prefer a counter to a bare number. +- Dates come from `Intl` and read year first: 2026年8月26日. Coordinates in prose are 北緯 / 東経; HUD hemisphere letters stay N, S, E, W. + +## Plurals + +`Intl.PluralRules('ja')` has one category, `other`: a `tn()` entry has only that form. Put the counter next to the number: 「航空機{n}機」, 「船舶{n}隻」, 「メッセージ {count}件」. + +## Glossary + +| English | 日本語 | +| ----------------------------------------- | --------------------------------------- | +| layer / data layer | レイヤー / データレイヤー | +| feed | フィード | +| source / data source / attribution | ソース / データソース / 出典 | +| LIVE (status) / live data | ライブ / ライブデータ | +| SIMULATED | シミュレーション | +| stale / degraded / unavailable | 更新遅延 / 性能低下 / 利用不可 | +| cached (status) | キャッシュの(キャッシュの予報) | +| PARTIAL (feed state) | 部分的 | +| upstream (remote data source) | 配信元 | +| retry | 再試行 | +| key (API key) / POWER UP | キー(APIキー)/ パワーアップ | +| Provider Settings | プロバイダー設定 | +| globe / map / map source / basemap | 地球儀 / 地図 / マップソース / ベースマップ | +| mapped (OSM-mapped sites) | マップ済み | +| aircraft / flight / military flights | 航空機 / フライト / 軍用機 | +| FLIGHT {count} (a rocket stage's nth flight) | {count}回目の飛行 | +| vessel, ship | 船舶 | +| satellite | 衛星 | +| launch (rocket) / space mission | 打ち上げ / 宇宙ミッション | +| stage (rocket) / downrange | 段 / ダウンレンジ | +| contact (tracked object) | コンタクト | +| installation, site | 施設 | +| camera / CCTV camera | カメラ / CCTVカメラ | +| earthquake / active fire / fire perimeter | 地震 / 火災検知 / 延焼範囲 | +| contained / containment (fire) | 封じ込め / 封じ込め率 | +| scene / shot | シーン / ショット | +| briefing / panel / dock | ブリーフィング / パネル / ドック | +| preset / visual preset | プリセット / ビジュアルプリセット | +| Tactical / Minimal / Cyber | タクティカル / ミニマル / サイバー | +| Scope (circular viewport mask) / keyhole | スコープ / キーホール | +| Clean UI | クリーンUI | +| cockpit | コックピット | +| heading / altitude / ground speed | 機首方位 / 高度 / 対地速度 | +| track (TRK, direction over the ground) | 対地針路 | +| HDG / BRG / CRS / FOV / SPD / POS | そのまま(HUDコード) | +| squawk | スコーク | +| radio failure / emergency (7600 / 7700) | 通信機故障 / 緊急事態 | +| map heading / North up | 方位 / 北を上に | +| Street Level / Street View (Google) | ストリートレベル / ストリートビュー | +| Recent Imagery | 最近の衛星画像 | +| Global Context / Context (panel) | グローバルコンテキスト / コンテキスト | +| radio (station) / tune / seek | ラジオ(局)/ 選局・同調 / シーク | +| Public Safety / SAFETY (radio category) | 警察・消防 | +| transit / bikeshare | 公共交通 / シェアサイクル | +| occupancy: empty / standing room only / crushed | 乗客なし / 立ち乗りのみ / 非常に混雑 | +| traffic flow (TomTom) | 交通流 | +| Previous / Next | 前へ / 次へ | +| SIG / NEWS / LOCAL (briefing tabs) | シグナル / ニュース / ローカル | +| compass points (N, NNE, NE, ENE, …) | 北, 北北東, 北東, 東北東, … | +| weather field / FIELD (selector) | 要素 | +| weather model / forecast lead hours | モデル / 予報時間 | +| valid (forecast time) / issued | 対象時刻 / 発表 | +| radar reflectivity / lightning density | レーダー反射強度 / 雷密度 | +| cyclone, storm (NHC/CPHC) / advisory | サイクロン / 情報(第N報); alone: サイクロン情報 | +| potential tropical cyclone (NHC PC) | サイクロン化のおそれがある擾乱 | +| forecast uncertainty cone | 予報円 | +| tropical storm / tropical depression | 熱帯暴風雨 / 熱帯低気圧 | +| scene cloud / mosaic / daily overview | シーン雲量 / モザイク / 日次概観 | +| box (Recent Imagery area) | 範囲 | +| WINDOW (search radius around a subject) | 範囲 | +| coverage / coverage % / full, partial | カバー範囲 / カバー率(%のみ)/ 全体, 一部 | +| pin (Recent Imagery) | 固定 | +| clear day (Recent Imagery) | 雲の少ない日 | +| outline / mark (map annotation) | 輪郭 / マーク | +| share (a view or link) | 共有 | +| import / export (project, presets) | インポート / エクスポート | +| EXPORT (image, PNG) | 書き出し | +| voice (the voice assistant) | 音声アシスタント / 音声機能 | +| activate (a control, screen-reader text) | 押す(「押すと…」) | +| AI agent | AIエージェント | +| frame (bring into view) | 視野に収める | +| story beat / evidence beat | ストーリー区切り / 証拠の区切り | +| camera pose / camera move | カメラの姿勢 / 移動 | +| roundabout / ramp / exit | ロータリー / ランプ / 出口 | +| reduced motion | 動きを減らす | +| NOT AN ALL-CLEAR | 安全の保証ではありません | +| … Watch (scene titles: Earth, Orbital) | 監視(地球監視, 軌道監視) | diff --git a/docs/translations/zh-CN.md b/docs/translations/zh-CN.md index e3b7bcad1..b3a3f973a 100644 --- a/docs/translations/zh-CN.md +++ b/docs/translations/zh-CN.md @@ -43,7 +43,7 @@ Translation sources: abiyeenzo (#914) and EDIT-Aeterna (#979). | briefing / panel / dock | 简报 / 面板 / 停靠栏 | | preset / visual preset | 预设 / 视觉预设 | | Display / HUD layout | 显示 / HUD 布局 | -| Tactical / Operator / Minimal / Cyber | 战术 / 操作员 / 精简 / 赛博 | +| Tactical / Minimal / Cyber | 战术 / 精简 / 赛博 | | Scope (circular viewport mask) / keyhole | 视域 | | Clean UI | 纯净界面 | | cockpit | 座舱 | diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 459bbcf80..2994c172f 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -561,6 +561,7 @@ "src/data/cctvCataloniaSource.test.mjs", "src/data/cctvHk.test.mjs", "src/data/cctvVegvesen.test.mjs", + "src/data/cctvA22.test.mjs", "src/data/cctvNyc.test.mjs", "src/data/cctvNswSource.test.mjs", "src/data/cctvTxdotSource.test.mjs", @@ -702,7 +703,6 @@ "src/ui/shareRestoration.js", "src/ui/visualSettings.js", "src/ui/panelChrome.js", - "src/ui/panelChrome.radioOwnership.test.mjs", "src/testSupport/readShellSource.mjs", "src/ui/stateOwners.test.mjs", "src/layers/flights/records.js", diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 1cdb8f154..985c411b6 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -439,8 +439,11 @@ "src/ui/slider.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -481,8 +484,11 @@ "src/localRequestGate.mjs", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -516,8 +522,11 @@ "src/data/layerSnapshot.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -541,8 +550,11 @@ "src/app/stateChannel.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -561,8 +573,11 @@ "src/ui/radioTunerModel.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -584,8 +599,11 @@ "src/ui/cctvPresentation.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -614,8 +632,11 @@ "src/ui/panelRailGeometry.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -652,8 +673,11 @@ "src/ui/panelRailGeometry.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -672,8 +696,11 @@ "src/ui/actionButton.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -835,8 +862,11 @@ "src/data/cameraArrival.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -961,8 +991,11 @@ "src/data/cameraArrival.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1034,8 +1067,11 @@ "src/data/modelEligibility.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1087,8 +1123,11 @@ "src/data/modelEligibility.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1154,8 +1193,11 @@ "src/data/inputOwnership.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1187,8 +1229,11 @@ "src/maps/imageryHost.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1224,8 +1269,11 @@ "src/data/inputOwnership.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1258,8 +1306,11 @@ "src/data/inputOwnership.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1295,8 +1346,11 @@ "src/data/inputOwnership.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1341,8 +1395,11 @@ "src/data/infrastructureOverlayEntry.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1381,8 +1438,11 @@ "src/ui/panelRailGeometry.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1435,8 +1495,11 @@ "src/layers/traffic/roadModes.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1468,8 +1531,11 @@ "src/data/cameraSensitivity.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1520,8 +1586,11 @@ "src/layers/cctv/sourcePolicy.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1587,8 +1656,11 @@ "scripts/qa-street-level.mjs", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1620,8 +1692,11 @@ "src/layers/alpr/floors.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1665,8 +1740,11 @@ "src/data/inputOwnership.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1691,8 +1769,11 @@ "src/data/inputOwnership.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1751,8 +1832,11 @@ "src/maps/imageryHost.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -1817,8 +1901,11 @@ "src/voice/pointerReticle.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -2707,8 +2794,11 @@ "src/app/embed.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -3299,8 +3389,11 @@ "src/sources/infrastructureData.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -3413,8 +3506,11 @@ "src/app/stateChannel.js", "src/data/cameraArrival.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -3441,8 +3537,11 @@ "src/ui/uiLifetime.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -3480,8 +3579,11 @@ "src/i18n/marker.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -3511,8 +3613,11 @@ "src/ui/uiLifetime.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -3606,8 +3711,11 @@ "src/data/routeSteps.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -3665,8 +3773,11 @@ "src/voice/pointerReticle.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -3923,8 +4034,11 @@ "src/sources/capability.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -3970,8 +4084,11 @@ "src/sources/transitHistory.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -4027,8 +4144,11 @@ "src/layers/perimeters/inciweb.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -4047,8 +4167,11 @@ "src/layers/gnss/records.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -4111,8 +4234,11 @@ "src/i18n/marker.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", @@ -4316,8 +4442,11 @@ "src/i18n/marker.js", "src/i18n/index.js", "src/i18n/locale.js", + "src/i18n/locales/de.json", "src/i18n/locales/es.json", + "src/i18n/locales/fr.json", "src/i18n/locales/it.json", + "src/i18n/locales/ja.json", "src/i18n/locales/pt.json", "src/i18n/locales/ru.json", "src/i18n/locales/uk.json", diff --git a/scripts/qa-cockpit-utility.mjs b/scripts/qa-cockpit-utility.mjs index 2bfc1ca41..e62df75f4 100644 --- a/scripts/qa-cockpit-utility.mjs +++ b/scripts/qa-cockpit-utility.mjs @@ -1751,7 +1751,7 @@ try { return state; }, { variantName: variant, kind: openKind }); - for (const variant of ['off', 'minimal', 'tactical', 'operator']) { + for (const variant of ['off', 'minimal', 'tactical']) { for (const kind of ['display', 'radio']) { const state = await desktopState(variant, kind); check( @@ -2081,7 +2081,7 @@ try { await page.screenshot({ path: path.join(shotsDir, `cyber-resize-${height}.png`) }); } } - await page.evaluate(() => window.__godsEyeView.styleManager._setHudVariant('operator')); + await page.evaluate(() => window.__godsEyeView.styleManager._setHudVariant('tactical')); const signalTransition = await page.evaluate(() => { const manager = window.__godsEyeView.styleManager; diff --git a/scripts/qa-firstrun-mutations.mjs b/scripts/qa-firstrun-mutations.mjs index 3ef331abd..a0a6d1ca9 100644 --- a/scripts/qa-firstrun-mutations.mjs +++ b/scripts/qa-firstrun-mutations.mjs @@ -175,12 +175,12 @@ const MUTATIONS = [ to: ' /* belt removed */', }, { - // Anchored past the call so it cannot land on the cockpit disclosure's own - // stopImmediatePropagation() a few lines below and prove nothing about this. - defect: 'the radio disclosure returns to stopPropagation, so one ESC does two things', + // Anchored past the call so it lands on the Cockpit utility disclosure's + // Escape claim and nowhere else. + defect: 'a Cockpit utility disclosure returns to stopPropagation, so later document listeners also act on its ESC', file: 'radio', - from: ' event.stopImmediatePropagation();\n const escapedFromDisclosure =\n event.target === this._contextRadioToggleBtn ||', - to: ' event.stopPropagation();\n const escapedFromDisclosure =\n event.target === this._contextRadioToggleBtn ||', + from: " event.stopImmediatePropagation();\n const kind = displayOpen ? 'display' : 'radio';", + to: " event.stopPropagation();\n const kind = displayOpen ? 'display' : 'radio';", }, { defect: 'the "no timer" decision is deleted, so the next editor re-litigates it blind', diff --git a/scripts/qa-firstrun.mjs b/scripts/qa-firstrun.mjs index 24d82bd2b..c8291ed62 100644 --- a/scripts/qa-firstrun.mjs +++ b/scripts/qa-firstrun.mjs @@ -357,53 +357,6 @@ async function runArbitrationSection(page, { shots, consoleErrors }) { `classVisible=${afterUncoveredEsc.classVisible} session=${afterUncoveredEsc.session}`, ); - // ── Blocker repro: a control that claims only the KEY ─────────────────── - // The compact Radio disclosure closes on ESC from a capture listener bound - // long before this module exists. It called stopPropagation(), which does - // NOT stop later listeners on the same document — so one key closed the - // disclosure AND dismissed the launcher. The card is not hiding behind this - // one and must not yield to it: the key is simply already spoken for. - await open(page, { query: "?welcome=1", errorSink: consoleErrors }); - const beforeRadio = await launcherState(); - record('the launcher is up before the radio disclosure opens', beforeRadio.onScreen); - const radioOpened = await page.evaluate(() => { - document.getElementById('context-radio-toggle-btn')?.click(); - return !!document.getElementById('context-radio-dock')?.classList.contains('disclosure-open'); - }); - await sleep(250); - record('the compact Radio disclosure opens', radioOpened); - const withRadio = await launcherState(); - record( - 'a small disclosure is a key contest, not a screen takeover — no yield', - withRadio.classVisible && withRadio.topmost, - `classVisible=${withRadio.classVisible} topmost=${withRadio.topmost}`, - ); - - await page.keyboard.press('Escape'); - await sleep(400); - const radioAfterEsc = await page.evaluate(() => ( - !!document.getElementById('context-radio-dock')?.classList.contains('disclosure-open') - )); - const launcherAfterRadioEsc = await launcherState(); - record('ESC closes the disclosure', radioOpened && !radioAfterEsc, - `disclosure still open=${radioAfterEsc}`); - record( - 'that SAME ESC does not also dismiss the launcher', - beforeRadio.onScreen && launcherAfterRadioEsc.classVisible - && launcherAfterRadioEsc.session !== 'dismissed', - `classVisible=${launcherAfterRadioEsc.classVisible} session=${launcherAfterRadioEsc.session} — one key, one action`, - ); - // ...and the NEXT press is the launcher's, so nothing was permanently taken. - await page.keyboard.press('Escape'); - await sleep(600); - const escAfterRadioClosed = await launcherState(); - record( - 'the next ESC belongs to the launcher again', - beforeRadio.onScreen && !escAfterRadioClosed.classVisible - && escAfterRadioClosed.session === 'dismissed', - `classVisible=${escAfterRadioClosed.classVisible} session=${escAfterRadioClosed.session}`, - ); - // ── Deferred reveal: a surface already up when the launcher would show ── // The class goes on as soon as the app exists, which is well before the // launcher's reveal (~T+1.9s), so init genuinely sees a screen it does not diff --git a/scripts/qa-radio.mjs b/scripts/qa-radio.mjs index d78c3dbcc..a71c69099 100644 --- a/scripts/qa-radio.mjs +++ b/scripts/qa-radio.mjs @@ -81,6 +81,32 @@ function check(name, ok, detail = '') { const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +// Radio lives only inside Context; a user reaches its controls by expanding +// Context and then Radio. Returns the prior collapse state so a step can put +// the panels back for the scenarios that follow. +async function revealRadioControls(page, targetId) { + const prior = await page.evaluate(async (id) => { + const manager = window.__godsEyeView.styleManager; + const collapsed = (panelId) => document.getElementById(panelId).classList.contains('collapsed'); + const state = { context: collapsed('global-context-panel'), radio: collapsed('radio-panel') }; + manager.setPanelCollapsed('global-context-panel', false, { explicit: true }); + manager.setPanelCollapsed('radio-panel', false, { explicit: true }); + await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); + document.getElementById(id).scrollIntoView({ block: 'nearest' }); + return state; + }, targetId); + await page.waitForSelector(`#${targetId}`, { visible: true }); + return prior; +} + +async function restoreRadioPanels(page, prior) { + await page.evaluate(({ context, radio }) => { + const manager = window.__godsEyeView.styleManager; + manager.setPanelCollapsed('radio-panel', radio); + manager.setPanelCollapsed('global-context-panel', context); + }, prior); +} + // Native pick buffers and clustered overlay entries settle on rendered frames, // not on wall-clock sleeps, particularly with software rendering. async function settleRadioFrames(page) { @@ -285,31 +311,16 @@ async function main() { window.__godsEyeView.styleManager.setPanelCollapsed('global-context-panel', true); }); + const enablePanels = await revealRadioControls(page, 'radio-enable-btn'); const enableStartedAt = Date.now(); - const initialDisclosure = await page.evaluate(() => { - const button = document.getElementById('context-radio-toggle-btn'); - const mini = document.getElementById('context-radio-mini'); - const enabledBefore = window.__godsEyeView.dataManager.isEnabled('radio'); - button.click(); - return { - enabledBefore, - enabledAfterDisclosure: window.__godsEyeView.dataManager.isEnabled('radio'), - expanded: button.getAttribute('aria-expanded'), - miniHidden: mini.hidden, - }; - }); - check( - 'Radio launcher discloses compact controls without changing power', - !initialDisclosure.enabledBefore && !initialDisclosure.enabledAfterDisclosure - && initialDisclosure.expanded === 'true' && !initialDisclosure.miniHidden, - JSON.stringify(initialDisclosure), - ); - await page.$eval('#context-radio-mini-enable-btn', (button) => button.click()); + await page.click('#radio-enable-btn'); await page.waitForFunction(() => { const state = window.__godsEyeView.dataManager.layers.get('radio').module.getUIState(); return state.enabled && !state.loading && state.stationCount === 750; }); const enableElapsedMs = Date.now() - enableStartedAt; + // Later scenarios start from the collapsed rail this step began with. + await restoreRadioPanels(page, enablePanels); const initial = await page.evaluate(() => { const module = window.__godsEyeView.dataManager.layers.get('radio').module; const state = module.getUIState(); @@ -328,9 +339,6 @@ async function main() { color: entry.dataset.radioColor, })), enablePressed: document.getElementById('radio-enable-btn').getAttribute('aria-pressed'), - launcherExpanded: document.getElementById('context-radio-toggle-btn').getAttribute('aria-expanded'), - launcherActive: document.getElementById('context-radio-toggle-btn').classList.contains('active'), - compactEnablePressed: document.getElementById('context-radio-mini-enable-btn').getAttribute('aria-pressed'), fullPlayEnabled: !document.getElementById('radio-play-btn').disabled, fullPlayLabel: document.getElementById('radio-play-btn').getAttribute('aria-label'), tunerVisible: !document.getElementById('radio-tuner').hidden, @@ -341,9 +349,8 @@ async function main() { check('750-station catalog renders within the browser budget', initial.state.stationCount === 750 && enableElapsedMs < 5000, `${enableElapsedMs}ms`); check('enable does not autoplay', initial.plays === 0 && initial.state.audioState === 'stopped'); check( - 'compact and detailed Radio power controls reflect the layer manager independently of disclosure', - initial.enablePressed === 'true' && initial.compactEnablePressed === 'true' - && initial.launcherExpanded === 'true' && initial.launcherActive, + 'Radio power control reflects the layer manager after the panels collapse again', + initial.enablePressed === 'true', ); check('dynamic canonical and genre categories reach the UI', initial.filterOptions >= 10, `${initial.filterOptions} options`); const musicOption = initial.filterPalette.find((entry) => entry.id === 'music'); @@ -419,14 +426,10 @@ async function main() { prevDisabled: document.getElementById('radio-prev-btn').disabled, playDisabled: document.getElementById('radio-play-btn').disabled, volumeDisabled: document.getElementById('radio-volume').disabled, - disclosureLabel: document.getElementById('context-radio-toggle-btn').getAttribute('aria-label'), - compactPowerLabel: document.getElementById('context-radio-mini-enable-btn').getAttribute('aria-label'), - compactPowerText: document.getElementById('context-radio-mini-enable-btn').textContent, fullLabel: document.getElementById('radio-enable-btn').getAttribute('aria-label'), fullText: document.getElementById('radio-enable-btn').textContent, layerState: document.getElementById('radio-layer-state').textContent, playbackMessage: document.getElementById('radio-playback-state').textContent, - miniStation: document.getElementById('context-radio-mini-station').textContent, dataRailText: document.querySelector('[data-layer-id="radio"] .data-toggle-btn')?.textContent, dataRailState: document.querySelector('[data-layer-id="radio"] .data-toggle-btn')?.dataset.feedState, dataRailLabel: document.querySelector('[data-layer-id="radio"] .data-toggle-btn')?.getAttribute('aria-label'), @@ -468,14 +471,10 @@ async function main() { && uncertainPresentation.prevDisabled && uncertainPresentation.playDisabled && uncertainPresentation.volumeDisabled - && /compact Radio controls/i.test(uncertainPresentation.disclosureLabel) - && /uncertain/i.test(uncertainPresentation.compactPowerLabel) - && uncertainPresentation.compactPowerText === 'RECONCILE' && /uncertain/i.test(uncertainPresentation.fullLabel) && uncertainPresentation.fullText === 'RECONCILE' && uncertainPresentation.layerState === 'UNCERTAIN' && /uncertain/i.test(uncertainPresentation.playbackMessage) - && uncertainPresentation.miniStation === 'RADIO STATE UNCERTAIN' && uncertainPresentation.dataRailText === 'UNCERTAIN' && uncertainPresentation.dataRailState === 'uncertain' && /uncertain/i.test(uncertainPresentation.dataRailLabel) @@ -517,10 +516,6 @@ async function main() { presentationActive: window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().presentationActive, markerSourceVisible: window.__godsEyeView.viewer.dataSources.getByName('Radio stations')[0]?.show, full: document.getElementById('radio-enable-btn').textContent, - disclosure: document.getElementById('context-radio-toggle-btn').getAttribute('aria-label'), - disclosureControls: document.getElementById('context-radio-toggle-btn').getAttribute('aria-controls'), - disclosureExpanded: document.getElementById('context-radio-toggle-btn').getAttribute('aria-expanded'), - compactPower: document.getElementById('context-radio-mini-enable-btn').textContent, layerState: document.getElementById('radio-layer-state').textContent, message: document.getElementById('radio-playback-state').textContent, stopDisabled: document.getElementById('radio-stop-btn').disabled, @@ -534,8 +529,6 @@ async function main() { && disablingPresentation.presentationActive === false && disablingPresentation.markerSourceVisible === false && disablingPresentation.full === 'DISABLING' - && /compact Radio controls/i.test(disablingPresentation.disclosure) - && disablingPresentation.compactPower === 'DISABLING' && disablingPresentation.layerState === 'DISABLING' && /disabling/i.test(disablingPresentation.message) && disablingPresentation.stopDisabled @@ -592,10 +585,6 @@ async function main() { presentationActive: window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().presentationActive, markerSourceVisible: window.__godsEyeView.viewer.dataSources.getByName('Radio stations')[0]?.show, full: document.getElementById('radio-enable-btn').textContent, - disclosure: document.getElementById('context-radio-toggle-btn').getAttribute('aria-label'), - disclosureControls: document.getElementById('context-radio-toggle-btn').getAttribute('aria-controls'), - disclosureExpanded: document.getElementById('context-radio-toggle-btn').getAttribute('aria-expanded'), - compactPower: document.getElementById('context-radio-mini-enable-btn').textContent, layerState: document.getElementById('radio-layer-state').textContent, message: document.getElementById('radio-playback-state').textContent, tunerHidden: document.getElementById('radio-tuner').hidden, @@ -610,10 +599,6 @@ async function main() { && enablingPresentation.presentationActive === false && enablingPresentation.markerSourceVisible === false && enablingPresentation.full === 'ENABLING' - && enablingPresentation.disclosure === 'Go to expanded Radio section' - && enablingPresentation.disclosureControls === 'radio-panel' - && enablingPresentation.disclosureExpanded === 'true' - && enablingPresentation.compactPower === 'ENABLING' && enablingPresentation.layerState === 'ENABLING' && /enabling/i.test(enablingPresentation.message) && enablingPresentation.tunerHidden @@ -1581,112 +1566,9 @@ async function main() { JSON.stringify(directoryRefreshContinuity), ); await page.select('#radio-filter', 'news'); - check('launcher enables Radio without forcing Context open', !initial.panelExpanded && !initial.contextExpanded && initial.nestedInContext); + check('enabled Radio stays nested in Context and leaves both panels collapsed', !initial.panelExpanded && !initial.contextExpanded && initial.nestedInContext); check('full Play is available before a station is selected', !initial.state.selected && initial.fullPlayEnabled && initial.fullPlayLabel === 'Play nearest radio station'); - const hoverOnlyDisclosure = await page.evaluate(() => { - const button = document.getElementById('context-radio-toggle-btn'); - if (button.getAttribute('aria-expanded') === 'true') button.click(); - return { - expanded: button.getAttribute('aria-expanded'), - hidden: document.getElementById('context-radio-mini').hidden, - }; - }); - await page.hover('#context-radio-toggle-btn'); - await sleep(100); - const hoverOnlyVisible = await page.$eval('#context-radio-mini', (mini) => ( - !mini.hidden && getComputedStyle(mini).display !== 'none' - )); - check( - 'hover alone does not disclose compact Radio controls', - hoverOnlyDisclosure.expanded === 'false' && hoverOnlyDisclosure.hidden && !hoverOnlyVisible, - JSON.stringify({ hoverOnlyDisclosure, hoverOnlyVisible }), - ); - await page.click('#context-radio-toggle-btn'); - await page.waitForFunction(() => { - const button = document.getElementById('context-radio-toggle-btn'); - const mini = document.getElementById('context-radio-mini'); - return button.getAttribute('aria-expanded') === 'true' - && !mini.hidden && getComputedStyle(mini).display !== 'none'; - }); - const compact = await page.evaluate(() => { - const mini = document.getElementById('context-radio-mini'); - const toggle = document.getElementById('context-radio-toggle-btn'); - const dock = document.getElementById('context-radio-dock'); - const miniRect = mini.getBoundingClientRect(); - const toggleRect = toggle.getBoundingClientRect(); - const miniStyle = getComputedStyle(mini); - return { - visible: miniStyle.display !== 'none' && miniRect.width > 0 && miniRect.height > 0, - display: miniStyle.display, - visibility: miniStyle.visibility, - opacity: miniStyle.opacity, - miniRect: { top: miniRect.top, left: miniRect.left, width: miniRect.width, height: miniRect.height }, - toggleRect: { top: toggleRect.top, left: toggleRect.left, width: toggleRect.width, height: toggleRect.height }, - dockRect: (() => { - const rect = dock.getBoundingClientRect(); - return { top: rect.top, left: rect.left, width: rect.width, height: rect.height }; - })(), - offsetParent: mini.offsetParent?.id || mini.offsetParent?.className || null, - prevEnabled: !document.getElementById('context-radio-mini-prev-btn').disabled, - playEnabled: !document.getElementById('context-radio-mini-play-btn').disabled, - playLabel: document.getElementById('context-radio-mini-play-btn').getAttribute('aria-label'), - nextEnabled: !document.getElementById('context-radio-mini-next-btn').disabled, - volumeEnabled: !document.getElementById('context-radio-mini-volume').disabled, - offsetBelowToggle: Math.round(miniRect.top - toggleRect.bottom), - }; - }); - check( - 'explicit launcher disclosure reveals enabled compact Radio controls', - compact.visible && compact.prevEnabled && compact.playEnabled && compact.nextEnabled && compact.volumeEnabled, - JSON.stringify(compact), - ); - check('compact Play is available before a station is selected', compact.playLabel === 'Play nearest radio station'); - check('compact Radio controls sit 20px below the launcher', compact.offsetBelowToggle === 20, `${compact.offsetBelowToggle}px`); - const compactCloseDisclosure = await page.evaluate(() => { - const launcher = document.getElementById('context-radio-toggle-btn'); - const close = document.getElementById('context-radio-mini-close-btn'); - close.focus(); - close.click(); - const result = { - launcherExpanded: launcher.getAttribute('aria-expanded'), - compactHidden: document.getElementById('context-radio-mini').hidden, - focusRestored: document.activeElement === launcher, - radioCollapsed: document.getElementById('radio-panel').classList.contains('collapsed'), - }; - launcher.click(); - return result; - }); - check( - 'dedicated compact close hides only the compact Radio disclosure and restores launcher focus', - compactCloseDisclosure.launcherExpanded === 'false' - && compactCloseDisclosure.compactHidden - && compactCloseDisclosure.focusRestored - && compactCloseDisclosure.radioCollapsed, - JSON.stringify(compactCloseDisclosure), - ); - const compactTouchDisclosure = await page.evaluate(() => { - const button = document.getElementById('context-radio-toggle-btn'); - button.click(); - const closed = button.getAttribute('aria-expanded') === 'false' - && document.getElementById('context-radio-mini').hidden; - button.dispatchEvent(new PointerEvent('pointerdown', { - bubbles: true, - pointerType: 'touch', - isPrimary: true, - })); - button.click(); - return { - closed, - reopened: button.getAttribute('aria-expanded') === 'true' - && !document.getElementById('context-radio-mini').hidden, - }; - }); - check( - 'touch/coarse-pointer activation can close and reopen compact Radio controls', - compactTouchDisclosure.closed && compactTouchDisclosure.reopened, - JSON.stringify(compactTouchDisclosure), - ); const multiExpandedPanelLayout = await page.evaluate(async () => { const manager = window.__godsEyeView.styleManager; const ids = ['data-panel', 'scene-panel', 'pp-toggles', 'cctv-panel', 'global-context-panel']; @@ -2062,7 +1944,6 @@ async function main() { hudVariant: manager.hud.getVariant(), displayExpanded: display.getAttribute('aria-expanded') === 'true', radioExpanded: radio.getAttribute('aria-expanded') === 'true', - contextRadioExpanded: document.getElementById('context-radio-toggle-btn')?.getAttribute('aria-expanded') === 'true', focusId: document.activeElement?.id || null, detectionMode: manager.getDetectionState().detectionMode, models3dEnabled: manager._models3dEnabled, @@ -2158,11 +2039,13 @@ async function main() { for (let index = 0; index < 5; index += 1) { recordLayoutStep(); if (index < 4) { - if (index < 2) { - hudLayout.value = index === 0 ? 'operator' : 'minimal'; - hudLayout.dispatchEvent(new Event('change', { bubbles: true })); - } else { + // Off and back on in Minimal, so the toggle must restore a + // non-default layout before the cycle returns to Tactical. + if (index === 1 || index === 2) { hudToggle.click(); + } else { + hudLayout.value = index === 0 ? 'minimal' : 'tactical'; + hudLayout.dispatchEvent(new Event('change', { bubbles: true })); } await waitForHudSettle(); } @@ -2287,8 +2170,7 @@ async function main() { document.getElementById('cockpit-radio-next-btn'), ].every((button) => button?.isConnected && !button.disabled); const detailsExcluded = !document.querySelector( - '#cockpit-radio-panel #context-radio-details-btn, ' - + '#cockpit-radio-panel #radio-filter, #cockpit-radio-panel #radio-tuner', + '#cockpit-radio-panel #radio-filter, #cockpit-radio-panel #radio-tuner', ); const scenesHidden = getComputedStyle(scene).display === 'none'; radio.click(); @@ -2331,7 +2213,7 @@ async function main() { scenesHidden, mapProviderUtilityStable, hudInteractionsExact: layoutSteps.map((step) => step.hud).join('>') - === 'TACTICAL>OPERATOR>MINIMAL>OFF>MINIMAL', + === 'TACTICAL>MINIMAL>OFF>MINIMAL>TACTICAL', // The accordion clears the topline, Cockpit cards, and Cesium credits // across every HUD layout. Its list owns any overflow internally. leftLaneClearAcrossHudCycle: layoutSteps.every((step) => ( @@ -2381,7 +2263,6 @@ async function main() { manager.setPanelCollapsed('global-context-panel', prior.contextCollapsed); if (display.getAttribute('aria-expanded') !== String(prior.displayExpanded)) display.click(); if (radio.getAttribute('aria-expanded') !== String(prior.radioExpanded)) radio.click(); - manager._setRadioDisclosure(prior.contextRadioExpanded); hud.hidden = prior.hudHidden; signal.hidden = prior.signalHidden; if (prior.utilityTop) hud.style.setProperty('--cockpit-utility-top', prior.utilityTop); @@ -2416,7 +2297,6 @@ async function main() { && document.getElementById('global-context-panel').classList.contains('collapsed') === prior.contextCollapsed && display.getAttribute('aria-expanded') === String(prior.displayExpanded) && radio.getAttribute('aria-expanded') === String(prior.radioExpanded) - && document.getElementById('context-radio-toggle-btn')?.getAttribute('aria-expanded') === String(prior.contextRadioExpanded) && hud.hidden === prior.hudHidden && signal.hidden === prior.signalHidden && intelHud.style.getPropertyValue('transition') === priorHudTransition @@ -2472,21 +2352,20 @@ async function main() { check('first Play scenario starts without a leaked Radio selection', firstPlayPrecondition === null, `selected=${firstPlayPrecondition}`); await page.select('#radio-filter', 'all'); - await page.$eval('#context-radio-toggle-btn', (button) => { - if (button.getAttribute('aria-expanded') !== 'true') button.click(); - }); await page.evaluate(() => { const camera = window.__godsEyeView.viewer.camera; window.__qaFirstPlayFlyToCalls = []; window.__qaFirstPlayOriginalFlyTo = camera.flyTo; camera.flyTo = (options) => window.__qaFirstPlayFlyToCalls.push(options); - document.getElementById('context-radio-toggle-btn').focus(); }); - await page.$eval('#context-radio-mini-play-btn', (button) => button.click()); + const firstPlayPanels = await revealRadioControls(page, 'radio-play-btn'); + await page.focus('#radio-play-btn'); + await page.click('#radio-play-btn'); await page.waitForFunction(() => { const state = window.__godsEyeView.dataManager.layers.get('radio').module.getUIState(); return state.audioState === 'playing' || state.audioState === 'error'; }); + await restoreRadioPanels(page, firstPlayPanels); const postMicroDragPlayback = await page.evaluate(async () => { const mod = window.__godsEyeView.dataManager.layers.get('radio').module; let state = mod.getUIState(); @@ -2593,59 +2472,19 @@ async function main() { ); await page.select('#radio-filter', 'news'); - await page.$eval('#context-radio-mini-volume', (input) => { + await page.$eval('#radio-volume', (input) => { input.value = '35'; input.dispatchEvent(new Event('input', { bubbles: true })); }); - const compactVolume = await page.evaluate(() => ({ + const radioVolume = await page.evaluate(() => ({ volume: window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().volume, full: document.getElementById('radio-volume').value, - mini: document.getElementById('context-radio-mini-volume').value, + label: document.getElementById('radio-volume-value').textContent, })); - check('compact volume shares the full Radio player state', Math.abs(compactVolume.volume - 0.35) < 0.001 && compactVolume.full === '35' && compactVolume.mini === '35'); - - const compactDisclosureContinuity = await page.evaluate(async () => { - const radio = window.__godsEyeView.dataManager.layers.get('radio').module; - const manager = window.__godsEyeView.styleManager; - manager.setPanelCollapsed('global-context-panel', true); - manager._setRadioDisclosure(true); - await radio.play({ origin: 'user' }); - const before = radio.getUIState(); - const disclosure = document.getElementById('context-radio-toggle-btn'); - if (disclosure.getAttribute('aria-expanded') !== 'true') disclosure.click(); - disclosure.click(); - const closed = { - expanded: disclosure.getAttribute('aria-expanded'), - hidden: document.getElementById('context-radio-mini').hidden, - state: radio.getUIState(), - }; - disclosure.click(); - return { - before, - closed, - reopened: disclosure.getAttribute('aria-expanded'), - miniHidden: document.getElementById('context-radio-mini').hidden, - after: radio.getUIState(), - }; - }); check( - 'compact disclosure preserves playback, station, filter, and volume continuity', - compactDisclosureContinuity.closed.expanded === 'false' - && compactDisclosureContinuity.closed.hidden - && compactDisclosureContinuity.reopened === 'true' - && !compactDisclosureContinuity.miniHidden - && compactDisclosureContinuity.before.audioState === 'playing' - && compactDisclosureContinuity.closed.state.audioState === 'playing' - && compactDisclosureContinuity.after.audioState === 'playing' - && compactDisclosureContinuity.after.selected?.id === compactDisclosureContinuity.before.selected?.id - && compactDisclosureContinuity.after.filter === compactDisclosureContinuity.before.filter - && compactDisclosureContinuity.after.volume === compactDisclosureContinuity.before.volume, - JSON.stringify({ - before: compactDisclosureContinuity.before, - closed: compactDisclosureContinuity.closed, - reopened: compactDisclosureContinuity.reopened, - after: compactDisclosureContinuity.after, - }), + 'full Radio volume drives the Radio player state', + Math.abs(radioVolume.volume - 0.35) < 0.001 && radioVolume.full === '35' && radioVolume.label === '35%', + JSON.stringify(radioVolume), ); const voicePause = await page.evaluate(async () => { @@ -2709,11 +2548,13 @@ async function main() { idleVoice.audioState === 'paused', idleVoice.audioState, ); - const manualPlayTarget = await page.evaluate(() => { + const manualPlayTarget = await page.evaluate(async () => { const manager = window.__godsEyeView.styleManager; - manager.setPanelCollapsed('global-context-panel', true); - manager._setRadioDisclosure(true); - const button = document.getElementById('context-radio-mini-play-btn'); + manager.setPanelCollapsed('global-context-panel', false); + manager.setPanelCollapsed('radio-panel', false); + await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); + const button = document.getElementById('radio-play-btn'); + button.scrollIntoView({ block: 'nearest' }); const rect = button.getBoundingClientRect(); const style = getComputedStyle(button); return { @@ -2728,12 +2569,12 @@ async function main() { }; }); check( - 'manual voice takeover targets the visible compact Radio Play control', + 'manual voice takeover targets the visible full Radio Play control', manualPlayTarget.visible && manualPlayTarget.enabled, JSON.stringify(manualPlayTarget), ); if (manualPlayTarget.visible && manualPlayTarget.enabled) { - await page.click('#context-radio-mini-play-btn'); + await page.click('#radio-play-btn'); await page.waitForFunction(() => ( window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().audioState === 'playing' && window.__godsEyeView.voiceCommands.status === 'idle' @@ -2781,7 +2622,6 @@ async function main() { await page.evaluate(() => window.__godsEyeView.voiceCommands.setStatus('idle', 'QA complete')); await page.evaluate(async () => { const manager = window.__godsEyeView.styleManager; - manager._setRadioDisclosure(false); manager.setPanelCollapsed('global-context-panel', false); manager.setPanelCollapsed('radio-panel', false); await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); @@ -3835,22 +3675,22 @@ async function main() { gev.dataManager.layers.get('flights').enabled = true; }); await sleep(500); - await page.evaluate(() => window.__godsEyeView.styleManager.setPanelCollapsed('global-context-panel', true)); await page.evaluate(() => { - const button = document.getElementById('context-radio-toggle-btn'); - if (button.getAttribute('aria-expanded') !== 'true') button.click(); + const manager = window.__godsEyeView.styleManager; + manager.setPanelCollapsed('global-context-panel', false); + manager.setPanelCollapsed('radio-panel', false); }); - await page.waitForSelector('#context-radio-mini', { visible: true }); + await page.waitForSelector('#radio-next-btn', { visible: true }); await page.evaluate(() => { window.__qaRadioCameraHeight = window.__godsEyeView.viewer.camera.positionCartographic.height; }); const radioOnlyNextDispatched = await page.evaluate(() => { - const button = document.getElementById('context-radio-mini-next-btn'); + const button = document.getElementById('radio-next-btn'); if (!button) return false; button.click(); return true; }); - if (!radioOnlyNextDispatched) throw new Error('Radio compact next button unavailable'); + if (!radioOnlyNextDispatched) throw new Error('Radio next button unavailable'); await page.waitForFunction(() => typeof window.__qaRejectRadioPlay === 'function'); await page.evaluate(() => { const viewer = window.__godsEyeView.viewer; @@ -3885,7 +3725,7 @@ async function main() { flyToCalls: window.__qaRadioFlyToCalls.length, flightTargets, cameraHeight: window.__qaRadioCameraHeight, - compactPlayLabel: document.getElementById('context-radio-mini-play-btn').getAttribute('aria-label'), + playLabel: document.getElementById('radio-play-btn').getAttribute('aria-label'), regularMarkerSize: Array.from({ length: window.__godsEyeView.viewer.dataSources.length }, (_, index) => window.__godsEyeView.viewer.dataSources.get(index)) .find((item) => item.name === 'Radio stations')?.entities.values[0]?.point?.pixelSize?.getValue(), selectedMarkerSize: selectedEntity?.point?.pixelSize?.getValue(), @@ -3898,7 +3738,7 @@ async function main() { }; }); check( - 'compact Next follows stable directory order and retries its next available station', + 'Radio Next follows stable directory order and retries its next available station', playing.state.selected?.id === playing.expectedFallback?.id && playing.calls[0] === playing.expectedPrimary?.streamUrl && playing.calls[1] === playing.expectedFallback?.streamUrl, @@ -3909,9 +3749,9 @@ async function main() { fallback: playing.expectedFallback?.id, }), ); - check('compact next is explicit playback and updates play/pause state', playing.calls.length === 2 && playing.compactPlayLabel.startsWith('Pause')); + check('Radio next is explicit playback and updates play/pause state', playing.calls.length === 2 && playing.playLabel.startsWith('Pause')); check( - 'compact next rotates toward its station without changing zoom, including fallback', + 'Radio next rotates toward its station without changing zoom, including fallback', playing.flyToCalls === 2 && Math.abs(playing.flightTargets[0]?.lat - playing.expectedPrimary?.lat) < 0.001 && Math.abs(playing.flightTargets[0]?.lon - playing.expectedPrimary?.lon) < 0.001 @@ -3950,11 +3790,11 @@ async function main() { }; return [ await run('flights', 'radio-next-btn'), - await run('military', 'context-radio-mini-prev-btn'), + await run('military', 'radio-prev-btn'), ]; }); check( - 'full and compact Radio navigation preserve Flights/Military camera ownership', + 'full Radio Next and Prev navigation preserve Flights/Military camera ownership', trackedRadioControls.every((sample) => sample.afterStationId && sample.afterStationId !== sample.beforeStationId && sample.flyToDelta === 0 @@ -3968,7 +3808,7 @@ async function main() { viewer.trackedEntity = undefined; window.__qaRadioDelayNextFailure = true; const beforeFlyToCalls = window.__qaRadioFlyToCalls.length; - document.getElementById('context-radio-mini-next-btn')?.click(); + document.getElementById('radio-next-btn')?.click(); return { beforeFlyToCalls }; }); await page.waitForFunction(() => typeof window.__qaRejectRadioPlay === 'function'); @@ -4244,7 +4084,7 @@ async function main() { window.__godsEyeView.dataManager.layers.get('flights').enabled = false; }); const radioOnlyNextClicked = await page.evaluate(() => { - const button = document.getElementById('context-radio-mini-next-btn'); + const button = document.getElementById('radio-next-btn'); if (!(button instanceof HTMLButtonElement)) return false; button.click(); return true; @@ -4508,106 +4348,12 @@ async function main() { && companion.radioCollapsed && !companion.contextCollapsed && companion.nested, ); - const expandedContextRadioBefore = await page.evaluate(() => { - const gev = window.__godsEyeView; - const scroller = document.querySelector('#global-context-panel .global-context-panel-inner'); - scroller.scrollTop = 0; - const camera = gev.viewer.camera.positionWC; - return { - scrollTop: scroller.scrollTop, - pageY: window.scrollY, - camera: { x: camera.x, y: camera.y, z: camera.z }, - plays: window.__qaRadioPlayCalls.length, - }; - }); - await page.click('#context-radio-toggle-btn'); - await page.waitForFunction(() => !document.getElementById('radio-panel').classList.contains('collapsed')); - await page.waitForFunction((priorScroll) => ( - document.activeElement?.getAttribute('data-collapse-target') === 'radio-panel' - && document.querySelector('#global-context-panel .global-context-panel-inner').scrollTop > priorScroll - ), { timeout: 10_000 }, expandedContextRadioBefore.scrollTop); - const expandedContextRadioAfter = await page.evaluate(() => { - const gev = window.__godsEyeView; - gev.styleManager._renderRadioState(gev.dataManager.layers.get('radio').module.getUIState()); - const launcher = document.getElementById('context-radio-toggle-btn'); - const radio = document.getElementById('radio-panel'); - const scroller = document.querySelector('#global-context-panel .global-context-panel-inner'); - const camera = gev.viewer.camera.positionWC; - return { - ariaControls: launcher.getAttribute('aria-controls'), - expanded: launcher.getAttribute('aria-expanded'), - label: launcher.getAttribute('aria-label'), - radioCollapsed: radio.classList.contains('collapsed'), - compactHidden: document.getElementById('context-radio-mini').hidden, - scrollTop: scroller.scrollTop, - focusTarget: document.activeElement?.getAttribute('data-collapse-target'), - pageY: window.scrollY, - camera: { x: camera.x, y: camera.y, z: camera.z }, - plays: window.__qaRadioPlayCalls.length, - state: gev.dataManager.layers.get('radio').module.getUIState().audioState, - }; - }); - const expandedContextRadioCameraDelta = Math.hypot( - expandedContextRadioAfter.camera.x - expandedContextRadioBefore.camera.x, - expandedContextRadioAfter.camera.y - expandedContextRadioBefore.camera.y, - expandedContextRadioAfter.camera.z - expandedContextRadioBefore.camera.z, - ); - check( - 'expanded Context Radio icon reveals and scrolls to embedded Radio without an overlay or playback/camera theft', - expandedContextRadioAfter.ariaControls === 'radio-panel' - && expandedContextRadioAfter.expanded === 'true' - && expandedContextRadioAfter.label === 'Go to expanded Radio section' - && !expandedContextRadioAfter.radioCollapsed - && expandedContextRadioAfter.compactHidden - && expandedContextRadioAfter.scrollTop > expandedContextRadioBefore.scrollTop - && expandedContextRadioAfter.focusTarget === 'radio-panel' - && expandedContextRadioAfter.pageY === expandedContextRadioBefore.pageY - && expandedContextRadioCameraDelta < 0.01 - && expandedContextRadioAfter.plays === expandedContextRadioBefore.plays - && expandedContextRadioAfter.state === 'playing', - JSON.stringify({ - before: expandedContextRadioBefore, - after: expandedContextRadioAfter, - cameraDelta: expandedContextRadioCameraDelta, - }), - ); - await page.evaluate(() => { const manager = window.__godsEyeView.styleManager; - manager.setPanelCollapsed('global-context-panel', true); - manager._setRadioDisclosure(true); + manager.setPanelCollapsed('global-context-panel', false); + manager.setPanelCollapsed('radio-panel', false); }); - await page.waitForFunction(() => { - const close = document.getElementById('context-radio-mini-close-btn'); - const rect = close.getBoundingClientRect(); - return !document.getElementById('context-radio-mini').hidden && rect.width > 0 && rect.height > 0; - }); - await page.click('#context-radio-mini-close-btn'); - const closedCompact = await page.evaluate(() => { - const launcher = document.getElementById('context-radio-toggle-btn'); - return { - radioCollapsed: document.getElementById('radio-panel').classList.contains('collapsed'), - compactHidden: document.getElementById('context-radio-mini').hidden, - focusRestored: document.activeElement === launcher, - }; - }); - await page.click('#context-radio-toggle-btn'); - await page.waitForFunction(() => !document.getElementById('context-radio-mini').hidden); - await page.click('#context-radio-details-btn'); - const repeatedOpen = await page.evaluate(() => ({ - radioCollapsed: document.getElementById('radio-panel').classList.contains('collapsed'), - compactHidden: document.getElementById('context-radio-mini').hidden, - })); - const splitDetailActions = { closedCompact, repeatedOpen }; - check( - 'collapsed Context keeps compact close and full-panel open as separate stable actions', - !splitDetailActions.closedCompact.radioCollapsed - && splitDetailActions.closedCompact.compactHidden - && splitDetailActions.closedCompact.focusRestored - && !splitDetailActions.repeatedOpen.radioCollapsed - && splitDetailActions.repeatedOpen.compactHidden, - JSON.stringify(splitDetailActions), - ); + await page.waitForFunction(() => !document.getElementById('radio-panel').classList.contains('collapsed')); await page.click('#global-context-flights-btn'); await page.waitForFunction(() => !document.getElementById('context-flights-view').hidden); diff --git a/scripts/qa-scope-hud.mjs b/scripts/qa-scope-hud.mjs index 7fece45ee..1607b2631 100644 --- a/scripts/qa-scope-hud.mjs +++ b/scripts/qa-scope-hud.mjs @@ -345,7 +345,7 @@ try { (await readScope()).feather === 2, ); await page.click('#scope-toggle'); - for (const layout of ['minimal', 'operator', 'cyber', 'tactical']) { + for (const layout of ['minimal', 'cyber', 'tactical']) { await page.select('#hud-layout-select', layout); const state = await readScope(); check( @@ -395,7 +395,7 @@ try { restored, ); - for (const layout of ['minimal', 'operator', 'tactical']) { + for (const layout of ['minimal', 'tactical']) { await page.select('#hud-layout-select', layout); await page.evaluate(() => window.__godsEyeView.styleManager.applyVisualState({ @@ -429,20 +429,13 @@ try { wrappersTransparent: wrappers.every( (el) => getComputedStyle(el).display === 'contents', ), - operatorFrame: - hud.dataset.variant !== 'operator' || - (getComputedStyle(hud.querySelector('.hud-corner')) - .backgroundColor === 'rgba(0, 0, 0, 0.36)' && - getComputedStyle(hud.querySelector('.hud-top-bar')) - .backgroundImage !== 'none'), }; }); check( `${layout}: Scope ON retains the original presentation without new backplates`, original.markerAbsent && original.unbacked && - original.wrappersTransparent && - original.operatorFrame, + original.wrappersTransparent, original, ); check( @@ -571,7 +564,7 @@ try { ); }); const broadBackings = [ - ...hud.querySelectorAll('.hud-corner, .hud-top-bar, .hud-bottom-bar'), + ...hud.querySelectorAll('.hud-corner, .hud-bottom-bar'), ].filter( (el) => el.offsetWidth > 0 && @@ -656,7 +649,7 @@ try { }), ); check('narrow HUD text backings stay within the viewport', narrow); - for (const layout of ['minimal', 'operator', 'tactical']) { + for (const layout of ['minimal', 'tactical']) { await page.select('#hud-layout-select', layout); await page.evaluate(() => window.__godsEyeView.styleManager.applyVisualState({ diff --git a/server/providers/cctv/catalog.js b/server/providers/cctv/catalog.js index 747aae4f8..1ab6b6aa2 100644 --- a/server/providers/cctv/catalog.js +++ b/server/providers/cctv/catalog.js @@ -28,6 +28,7 @@ import { loadNycSourcesFromOpenData, loadTorontoSourcesFromOpenData, loadFloridaSourcesFromOpenData, + loadA22SourcesFromOpenDataHub, } from './sources.js'; /** Env kill switch: unset or anything but "0" means enabled. */ @@ -165,6 +166,11 @@ const LIVE_PACKS = [ enabled: () => envEnabled('CCTV_TAIWAN_ENABLED'), load: loadTaiwanSources, }, + { + name: 'a22', + enabled: () => envEnabled('CCTV_A22_ENABLED'), + load: loadA22SourcesFromOpenDataHub, + }, ]; /** * Load CCTV sources from a local JSON file (CCTV_SOURCES_FILE env or default). diff --git a/server/providers/cctv/constants.js b/server/providers/cctv/constants.js index a26fa6c20..74711cb0c 100644 --- a/server/providers/cctv/constants.js +++ b/server/providers/cctv/constants.js @@ -443,6 +443,39 @@ export const NYC_ANCHORS = [ { lat: 40.5795, lon: -74.1502 }, // Staten Island ]; +/** Autostrada del Brennero (A22, Brenner–Modena) webcams: the keyless Open + * Data Hub (NOI Techpark, South Tyrol) tourism API republishes Autobrennero's + * own camera list with coordinates, one request for the whole motorway + * (13 cameras). Frames are stills on the operator's host, refreshed about + * once a minute. */ +export const DEFAULT_A22_CCTV_URL = + 'https://tourism.api.opendatahub.com/v1/WebcamInfo?source=a22&pagesize=200&removenullvalues=true'; +/** The only origin A22 camera frames may come from: `km.jpg` per camera. */ +export const A22_IMAGE_ORIGIN = 'https://www.autobrennero.it/WebCamImg/'; +/** Hard ceiling on the catalog body. The whole motorway is ~45 KB. */ +export const A22_MAX_CATALOG_BYTES = 2 * 1024 * 1024; +/** Orthometric ground elevation (m) at each camera, keyed by its motorway + * kilometre, from the Copernicus GLO-90 DEM via Open-Meteo's elevation API. + * The feed's coordinates are 2D and the road climbs from the Po valley (~20 m) + * to the Brenner Pass (~1,370 m), so one flat prior would bury the Alpine + * cameras. A camera not listed here uses A22_DEFAULT_GROUND_ELEVATION_M. */ +export const A22_GROUND_ELEVATION_BY_KM = Object.freeze({ + 1: 1365, + 68: 375, + 77: 301, + 100: 219, + 129: 206, + 138: 204, + 159: 175, + 205: 188, + 228: 72, + 240: 38, + 272: 16, + 285: 19, + 313: 40, +}); +export const A22_DEFAULT_GROUND_ELEVATION_M = 200; + /** DelDOT CCTV: one keyless statewide JSON catalog; live video via RTMP-over-HTTP (rtmpt:80). */ export const DELDOT_CCTV_URL = 'https://tmc.deldot.gov/json/videocamera.json'; export const DEFAULT_DELDOT_MAX_SOURCES = 300; diff --git a/server/providers/cctv/media.js b/server/providers/cctv/media.js index 206a24888..7f89f2ae1 100644 --- a/server/providers/cctv/media.js +++ b/server/providers/cctv/media.js @@ -1,6 +1,7 @@ import { Readable } from 'node:stream'; import { hashSeed, escapeXml, pinnedCameraUrl } from './normalize.js'; import { + A22_IMAGE_ORIGIN, CALGARY_IMAGE_ORIGIN, CCTV_FRAME_FETCH_TIMEOUT_MS, CCTV_FRAME_MAX_BODY_BYTES, @@ -528,6 +529,7 @@ for (const prefix of [ HK_IMAGE_ORIGIN, KING_COUNTY_IMAGE_ORIGIN, WSDOT_IMAGE_ORIGIN, + A22_IMAGE_ORIGIN, ]) { const host = new URL(prefix).hostname; CCTV_PINNED_PREFIXES_BY_HOST.set(host, [ diff --git a/server/providers/cctv/normalize.js b/server/providers/cctv/normalize.js index f190f257e..a071d640c 100644 --- a/server/providers/cctv/normalize.js +++ b/server/providers/cctv/normalize.js @@ -421,6 +421,17 @@ export function isLikelyNorwayCoordinate(lat, lon) { ); } +/** The A22 corridor, Brenner Pass to Modena, with slack either side. */ +export function isLikelyA22Coordinate(lat, lon) { + return ( + isPlausibleLatLon(lat, lon) && + lat >= 44.5 && + lat <= 47.1 && + lon >= 10.5 && + lon <= 11.8 + ); +} + export function isLikelyFinlandCoordinate(lat, lon) { if (!Number.isFinite(lat) || !Number.isFinite(lon)) return false; return lat >= 59.5 && lat <= 70.5 && lon >= 19 && lon <= 32; diff --git a/server/providers/cctv/sources.js b/server/providers/cctv/sources.js index a479a945c..e22ea916f 100644 --- a/server/providers/cctv/sources.js +++ b/server/providers/cctv/sources.js @@ -122,6 +122,11 @@ import { NYC_MAX_CATALOG_BYTES, NYC_GROUND_ELEVATION_M, NYC_ANCHORS, + DEFAULT_A22_CCTV_URL, + A22_IMAGE_ORIGIN, + A22_MAX_CATALOG_BYTES, + A22_GROUND_ELEVATION_BY_KM, + A22_DEFAULT_GROUND_ELEVATION_M, } from './constants.js'; import { toFiniteNumber, @@ -145,6 +150,7 @@ import { isLikelyHongKongCoordinate, isLikelyNorwayCoordinate, isLikelyTorontoCoordinate, + isLikelyA22Coordinate, cameraDisplayCode, rowArrayToObject, prioritizeSources, @@ -3645,3 +3651,123 @@ export async function loadFloridaSourcesFromOpenData() { return []; } } + +/** + * One Open Data Hub `WebcamInfo` item (source `a22`) -> one catalog source, + * or null. + * + * The frame must be the operator's own `WebCamImg/km.jpg` for the + * camera's motorway kilometre. The feed names the place and the kilometre but + * no bearing, so the heading uses the shared id-hash fallback at low + * confidence, like Calgary and Vegvesen; the ground elevation comes from the + * per-kilometre DEM table because the road spans ~1,350 m of altitude. + * + * @param {object} item - One element of the API's `Items` array. + * @returns {?object} + */ +export function a22WebcamToSource(item) { + if (!item || typeof item !== 'object') return null; + if (item.Active === false) return null; + if (String(item.Source ?? '').toLowerCase() !== 'a22') return null; + + const gps = Array.isArray(item.GpsInfo) ? item.GpsInfo[0] : null; + const lat = toFiniteNumber(gps?.Latitude); + const lon = toFiniteNumber(gps?.Longitude); + if (!isLikelyA22Coordinate(lat, lon)) return null; + + const km = String(item.Mapping?.a22?.km ?? '').trim(); + if (!/^\d{1,3}$/.test(km)) return null; + const imageUrl = `${A22_IMAGE_ORIGIN}km${Number(km)}.jpg`; + let image; + try { + image = new URL(String(item.Webcamurl ?? '')); + } catch { + return null; + } + if (image.username || image.password || image.href !== imageUrl) return null; + + const place = + String(item.Shortname ?? item.Webcamname?.it ?? '') + .replace(/\s+/g, ' ') + .trim() || `Km ${Number(km)}`; + const cameraId = `it-a22-km${Number(km)}`; + const ground = A22_GROUND_ELEVATION_BY_KM[Number(km)]; + + return { + id: cameraId, + name: `A22 ${place} (km ${Number(km)})`, + // One motorway-wide category in the camera picker; the place is already + // in the name. + city: 'A22 Brennero', + cityId: 'a22', + provider: 'Autostrada del Brennero', + lat, + lon, + headingDeg: fallbackHeadingFromId(cameraId), + headingConfidence: 'low', + pitchDeg: -15, + fovDeg: 50, + rangeM: 160, + mountHeightM: 8, + groundElevationM: Number.isFinite(ground) + ? ground + : A22_DEFAULT_GROUND_ELEVATION_M, + feedType: 'image', + url: imageUrl, + snapshotUrl: imageUrl, + sourceKind: 'a22-opendatahub', + license: + 'Webcam images © Autostrada del Brennero S.p.A., via the Open Data Hub (NOI Techpark)', + code: cameraDisplayCode(place.toUpperCase()), + }; +} + +/** + * Fetch the Autostrada del Brennero (A22) webcams from the keyless Open Data + * Hub tourism API. Frames are stills on www.autobrennero.it. + * + * @returns {Promise>} Normalized camera source objects. + */ +export async function loadA22SourcesFromOpenDataHub() { + try { + const endpoint = process.env.CCTV_A22_URL || DEFAULT_A22_CCTV_URL; + const resp = await fetch(endpoint, { + headers: { Accept: 'application/json' }, + redirect: 'manual', + signal: AbortSignal.timeout(CCTV_SOURCE_FETCH_TIMEOUT_MS), + }); + const discard = async () => { + try { + await resp.body?.cancel(); + } catch { + /* no-op */ + } + return []; + }; + if (resp.status >= 300 && resp.status < 400) { + console.warn('[CCTV] A22 catalog redirected; redirects are not followed'); + return discard(); + } + if (!resp.ok) { + console.warn('[CCTV] A22 camera download failed:', resp.status); + return discard(); + } + const payload = await readResponseJsonCapped(resp, A22_MAX_CATALOG_BYTES); + const items = Array.isArray(payload?.Items) ? payload.Items : []; + const cameras = []; + const seen = new Set(); + for (const item of items) { + const camera = a22WebcamToSource(item); + if (!camera || seen.has(camera.id)) continue; + seen.add(camera.id); + cameras.push(camera); + } + // North to south along the motorway, so the picker reads in road order. + cameras.sort((a, b) => b.lat - a.lat); + console.log(`[CCTV] Loaded A22 camera sources: ${cameras.length}`); + return cameras; + } catch (error) { + console.warn('[CCTV] A22 camera download error:', error?.message || error); + return []; + } +} diff --git a/server/providers/firms.js b/server/providers/firms.js index c73ca10ce..20a2f4bab 100644 --- a/server/providers/firms.js +++ b/server/providers/firms.js @@ -1,9 +1,132 @@ import path from 'node:path'; +import https from 'node:https'; import { promises as fsp } from 'node:fs'; +import { Readable } from 'node:stream'; import { filterTrailing24h, parseFirmsCsv } from '../../src/data/firmsCsv.js'; import { readResponseTextCapped } from './common/http.js'; +const NULL_BODY_STATUSES = new Set([204, 205, 304]); + +/** + * The per-address connection errors behind a fetch failure, or null. Node 24 + * puts the AggregateError directly on `err.cause`; Node 26's fetch wraps it in + * a ConnectTimeoutError one level deeper. + * @param {unknown} err + * @returns {?Array} + */ +function connectAttemptErrors(err) { + let cause = err?.cause; + for (let depth = 0; cause && depth < 3; depth++) { + if (Array.isArray(cause.errors)) return cause.errors; + cause = cause.cause; + } + return null; +} + +/** + * Describe a fetch failure with its socket-level cause. Node reports every + * connection failure as `fetch failed`; the cause carries the error code and + * the address that failed (never the URL, which embeds the MAP_KEY). + * @param {unknown} err + * @returns {string} + */ +export function describeFetchError(err) { + const message = err?.message || String(err); + const cause = err?.cause; + if (!cause) return message; + const attempts = connectAttemptErrors(err); + const parts = attempts?.length + ? attempts.map((inner) => inner?.message || inner?.code || String(inner)) + : [cause.message || cause.code || String(cause)]; + return `${message} (${parts.join('; ')})`; +} + +/** + * Whether a fetch failed because every address it tried failed to connect. + * Node's network-family auto-selection gives each address a short attempt + * timeout, so a slow IPv4 handshake is abandoned in favour of an unreachable + * IPv6 address and the whole request fails (#68). + * @param {unknown} err + * @returns {boolean} + */ +export function isAddressSelectionFailure(err) { + return err instanceof TypeError && connectAttemptErrors(err)?.length > 0; +} + +/** + * GET over IPv4 only, with one connection attempt bounded by the caller's + * signal rather than by the auto-selection attempt timeout. + * @param {string} url + * @param {{signal?: AbortSignal}} [options] + * @returns {Promise} + */ +export function fetchIpv4(url, { signal } = {}) { + return new Promise((resolve, reject) => { + const request = https.request( + url, + { + method: 'GET', + signal, + family: 4, + autoSelectFamily: false, + agent: false, + }, + (response) => { + // Nothing thrown here may escape: this callback runs outside the + // promise, so an exception would be uncaught and crash the server. + try { + const status = response.statusCode || 500; + // A `Response` only represents statuses up to 599; Node never hands + // this callback a 1xx. + if (status > 599) + throw new Error(`FIRMS returned unsupported status ${status}`); + const headers = new Headers(); + for (const [name, value] of Object.entries(response.headers)) { + if (Array.isArray(value)) + value.forEach((item) => headers.append(name, item)); + else if (value !== undefined) headers.set(name, String(value)); + } + const nullBody = NULL_BODY_STATUSES.has(status); + const upstream = new Response( + nullBody ? null : Readable.toWeb(response), + { status, statusText: response.statusMessage || '', headers }, + ); + if (nullBody) response.resume(); + resolve(upstream); + } catch (error) { + response.destroy(); + reject(error); + } + }, + ); + request.on('error', reject); + request.end(); + }); +} + +/** + * Fetch a FIRMS URL, retrying once over IPv4 when address selection fails. + * Other failures (HTTP status, timeout, DNS) are returned or thrown as is. + * @param {string} url + * @param {{signal?: AbortSignal}} options + * @param {{fetchImpl?: typeof fetch, fetchIpv4Impl?: typeof fetchIpv4, warn?: Function}} [deps] + * @returns {Promise} + */ +export async function fetchFirms( + url, + options, + { fetchImpl = fetch, fetchIpv4Impl = fetchIpv4, warn = console.warn } = {}, +) { + try { + return await fetchImpl(url, options); + } catch (err) { + if (!isAddressSelectionFailure(err) || options?.signal?.aborted) throw err; + warn(`[firms-proxy] ${describeFetchError(err)} — retrying over IPv4`); + return fetchIpv4Impl(url, options); + } +} + /** * NASA FIRMS live active-fire proxy with a memory + disk cache. * Upstream: https://firms.modaps.eosdis.nasa.gov/api/area/csv/{KEY}/{SOURCE}/world/2 @@ -91,8 +214,12 @@ export function firmsProxy() { async function fetchSource(key, source) { const url = `https://firms.modaps.eosdis.nasa.gov/api/area/csv/${encodeURIComponent(key)}/${source}/world/2`; const signal = AbortSignal.timeout(60_000); - const res = await fetch(url, { signal }); - if (!res.ok) throw new Error(`HTTP ${res.status}`); + const res = await fetchFirms(url, { signal }); + if (!res.ok) { + // Release the connection now rather than when the deadline fires. + await res.body?.cancel().catch(() => {}); + throw new Error(`HTTP ${res.status}`); + } const records = parseFirmsCsv( await readResponseTextCapped(res, FIRMS_MAX_RESPONSE_BYTES, signal), ); @@ -121,7 +248,7 @@ export function firmsProxy() { } catch (err) { console.warn( `[firms-proxy] ${source} fetch failed:`, - err?.message || err, + describeFetchError(err), ); sources.push({ source, count: 0, ok: false }); } @@ -156,8 +283,13 @@ export function firmsProxy() { statusInflight = (async () => { try { const url = `https://firms.modaps.eosdis.nasa.gov/mapserver/mapkey_status/?MAP_KEY=${encodeURIComponent(key)}`; - const res = await fetch(url, { signal: AbortSignal.timeout(10_000) }); - if (!res.ok) throw new Error(`HTTP ${res.status}`); + const res = await fetchFirms(url, { + signal: AbortSignal.timeout(10_000), + }); + if (!res.ok) { + await res.body?.cancel().catch(() => {}); + throw new Error(`HTTP ${res.status}`); + } const body = await res.json(); const used = Number(body?.current_transactions); const limit = Number(body?.transaction_limit); @@ -167,7 +299,7 @@ export function firmsProxy() { } catch (err) { console.warn( '[firms-proxy] mapkey status failed:', - err?.message || err, + describeFetchError(err), ); return null; } diff --git a/server/providers/openai/instructions.js b/server/providers/openai/instructions.js index 2841df78a..acf576d13 100644 --- a/server/providers/openai/instructions.js +++ b/server/providers/openai/instructions.js @@ -48,13 +48,13 @@ function realtimeInstructions( 'For visual filter requests, call set_visual_style with one of the allowed style IDs.', 'SHIPS vs STYLES: "vessels" is easily heard as "visuals". "Show me vessels", "turn on vessels", "the vessels", "visuals" or "vestibules" said WITHOUT a style name means the ships layer: set_layer_visibility{layerId:"ais-live-vessels"}, exactly as "ships" or "boats" does. Call set_visual_style only when the user names a style (retro/CRT, surveillance/night vision, thermal, anime, noir, normal) or says filter, effect or look. Never turn on a different data layer (such as fires) for a ships request.', 'Disambiguation table — basemap vs layer vs style: basemap switching requires an explicit stack name — "Bing aerial" means set_map_stack bing-aerial, "aerial with labels" means bing-labels, "OSM"/"road map" means osm, "Esri"/"Esri imagery" means esri-imagery, "Google 3D"/"photorealistic" means photoreal. Any other mention of "satellite" or "satellites" means the satellites DATA LAYER via set_layer_visibility, never a basemap — except "recent/latest satellite imagery" (the recent-imagery layer) and "weather satellite"/clouds (weather-satellite). "surveillance"/"night vision"/"thermal" are visual STYLES via set_visual_style.', - 'HUD requests ("hud on/off", "switch to operator/minimal/tactical/cyber layout") use set_hud. Detection requests ("detection on", "dense mode", "balanced mode", "sparse mode", "set density to 25", "use weighted allocation") use set_detection. Density snaps to 0/25/50/75/100 and derives Sparse/Balanced/Dense; panoptic is a legacy alias for Dense.', + 'HUD requests ("hud on/off", "switch to minimal/tactical/cyber layout") use set_hud. Detection requests ("detection on", "dense mode", "balanced mode", "sparse mode", "set density to 25", "use weighted allocation") use set_detection. Density snaps to 0/25/50/75/100 and derives Sparse/Balanced/Dense; panoptic is a legacy alias for Dense.', 'Cyber sonar requests use set_cyber_sonar: enabled, rings, rangePct, intensityPct (Power), opacityPct, sectorDeg. For settings questions read get_current_view_state.controls.hud.sonar. Sonar is not detection. Do not silently switch themes, reveal the HUD or enable sonar when changing a slider. If the user explicitly requests Cyber plus sonar settings, call set_hud first, wait for success, then set_cyber_sonar. Confirm returned values; mapSweepActive and contactSweepActive distinguish saved settings from active map effects, including in Cockpit or reduced motion. Do not claim visible contacts solely because enabled is true.', 'Bloom/sharpen requests use set_post_processing. Scene requests ("play orbital watch", "stop the scene", "what scenes are there") use control_scene. CCTV camera requests ("next camera", "nearest camera", "select the Congress camera", "show coverage") use control_cctv — the CCTV layer must be enabled first.', 'Radio playback requests use control_radio. "Turn on/start the radio" means action=play; action=enable only reveals Radio markers and must be reserved for explicit "show/enable the Radio layer/markers" requests. After a prepared playback result, briefly confirm any other completed actions and say "Turning on the radio"—never claim it is already playing. The client keeps Radio muted until playback is verified, then closes voice before restoring Radio volume. Examples: "play news near Austin" → select category=news locationId=austin; "play US news" → select category=news country=US; "Radio volume 30" → volume; pause/resume/stop/next/previous use the matching action. Radio selection never moves the camera.', '"Track/follow " (a callsign, ship name, satellite name) uses track_entity. "Take me to the biggest fire" uses track_entity with query "biggest fire" (the fires layer must be enabled). Bare "orbit" means camera orbit of the current landmark. "Stop following/tracking" uses stop_tracking.', '"Show me which planes are overhead"/"frame the ships"/"show me the satellites above" use frame_overhead with the matching target.', - 'Confirmations echo the RESULTING state, never the request: "HUD operator layout", "Density twenty-five percent", "Bing aerial imagery", "Tracking UAL428", "Framed fourteen aircraft". On ok=false, state the failure plainly: "Nothing matched UAL999", "No ships within 120 kilometers". Never claim an action without ok=true in the tool result.', + 'Confirmations echo the RESULTING state, never the request: "HUD minimal layout", "Density twenty-five percent", "Bing aerial imagery", "Tracking UAL428", "Framed fourteen aircraft". On ok=false, state the failure plainly: "Nothing matched UAL999", "No ships within 120 kilometers". Never claim an action without ok=true in the tool result.', 'For destination requests such as "take me to Italy", "go to NYC", or "show me the Eiffel Tower", call fly_to_location. Prefer known city IDs when available; otherwise pass the plain place query.', 'Navigation-only requests ("take me to X", "go to X", "fly to X") are NOT descriptions: call fly_to_location alone and do NOT also call annotate_map, unless the user explicitly asks to mark the place or you go on to explain specific places there. Never drop a point pin on a region-scale natural feature (a mountain range, desert, sea, or forest) — a single point in the middle of the Rockies is meaningless. If the user explicitly asks to mark such a region, prefer type=area.', 'For country and city destinations, omit rangeM so GEV frames the whole country or city in view. For landmarks and buildings, omit rangeM so GEV chooses a close landmark view.', diff --git a/src/cockpitMarkup.test.mjs b/src/cockpitMarkup.test.mjs index c47b29c5c..075b5a0ad 100644 --- a/src/cockpitMarkup.test.mjs +++ b/src/cockpitMarkup.test.mjs @@ -227,13 +227,9 @@ test('Cockpit Escape handling precedes form-control shortcut suppression and foc ); }); -test('Cockpit shortcut failures do not leak and open Radio owns the first Escape', () => { +test('Cockpit shortcut failures do not leak and open utilities own the first Escape', () => { const keydown = cockpitKeyDown.toString(); assert.ok(keydown, 'Cockpit keyboard handler is missing'); - assert.match( - keydown, - /document\s*\.getElementById\('context-radio-dock'\)\s*\?\.classList\.contains\('disclosure-open'\)/, - ); assert.match(keydown, /#cockpit-utility-controls \[aria-expanded="true"\]/); assert.match( keydown, @@ -443,7 +439,7 @@ test('Cockpit owns a focused shared Display portal and compact Radio controls', assert.ok(cockpitRadio, 'Cockpit compact Radio controls are missing'); assert.doesNotMatch( cockpitRadio[0], - /context-radio-details-btn|radio-filter|radio-tuner/, + /radio-filter|radio-tuner/, ); assert.match( css, @@ -898,7 +894,7 @@ test('Cockpit Radio station changes preserve first-person camera ownership', () ); assert.match( radioBindings, - /listen\(this\._contextRadioMiniNextBtn, 'click', \(\) => cycleRadio\(1\)\)/, + /listen\(this\._radioNextBtn, 'click', \(\) => cycleRadio\(1\)\)/, ); assert.match( radioBindings, diff --git a/src/contextTabKeyboard.test.mjs b/src/contextTabKeyboard.test.mjs index 6994459a7..528341dd4 100644 --- a/src/contextTabKeyboard.test.mjs +++ b/src/contextTabKeyboard.test.mjs @@ -129,7 +129,7 @@ test('Context tabs draw a visible keyboard-focus outline including active tabs', test('Context async action buttons remain focused while busy', () => { const init = _initGlobalContextPanel.toString(); - const radio = radioBindings.slice(radioBindings.indexOf('const toggleRadio = async (trigger) => {'), radioBindings.indexOf('this.listen(this._contextRadioToggleBtn,')); + const radio = radioBindings.slice(radioBindings.indexOf('const toggleRadio = async (trigger) => {'), radioBindings.indexOf('this.listen(this._cockpitRadioToggleBtn,')); const radioSync = radioPresentation.slice(0, radioPresentation.indexOf('if (this._radioFilter)')); const clear = clearSelectedLayers.toString(); @@ -137,7 +137,7 @@ test('Context async action buttons remain focused while busy', () => { assert.doesNotMatch(init, /button\.disabled\s*=\s*true/); assert.match(radio, /trigger\.getAttribute\('aria-busy'\) === 'true'/); assert.doesNotMatch(radio, /trigger\.disabled\s*=\s*true/); - for (const name of ['_radioEnableBtn', '_contextRadioMiniEnableBtn', '_cockpitRadioEnableBtn']) { + for (const name of ['_radioEnableBtn', '_cockpitRadioEnableBtn']) { assert.match(radioSync, new RegExp(`${name}\\.disabled = false`)); } assert.doesNotMatch(clear, /_clearSelectedLayersBtn\.disabled\s*=\s*true/); @@ -146,3 +146,36 @@ test('Context async action buttons remain focused while busy', () => { assert.match(control, /button\.setAttribute\('aria-busy', String\(busy\)\)/); assert.doesNotMatch(control, /button\.disabled\s*=/); }); + +test('Context header holds only its title and collapse control', () => { + const panel = html.match(/
([\s\S]*?)<\/div>\s*
`${tag}.${cls}`); + assert.deepEqual(tags, ['span.panel-title', 'span.panel-divider', 'button.panel-collapse-btn']); +}); + +test('Context reads as one segmented control with legible type and no halo', () => { + const rules = [...css.matchAll(/([^{}]+)\{([^{}]*)\}/g)]; + const clean = (selector) => selector.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\s+/g, ' ').trim(); + const rule = (selector) => rules.find(([, s]) => clean(s) === selector)?.[2] || ''; + const px = (body, prop) => Number(body.match(new RegExp(`${prop}:[^;]*?(\\d+(?:\\.\\d+)?)px`))?.[1] || 0); + + assert.match(rule('#global-context-panel .panel-glow'), /display:\s*none/); + const modes = rule('.global-context-modes'); + assert.match(modes, /border:\s*1px solid/); + assert.match(modes, /padding:\s*2px/); + assert.ok(px(rule('.context-mode-button'), 'font') >= 10, 'mode labels must stay at least 10px'); + assert.doesNotMatch(rule('.context-mode-button.active'), /box-shadow/); + const standby = rule('.context-mode-standby'); + assert.ok(px(standby, 'font') >= 10, 'standby guidance must stay at least 10px'); + assert.doesNotMatch(standby, /dashed/); + assert.ok(px(rule('.global-context-actions .panel-layer-toggle'), 'font-size') >= 10); + // The shared rail title colour (brighter than --text-secondary) keeps the + // open Context title legible; no Context-only rule dims it again. + assert.equal( + rule(":root:not([data-ui-theme='cyber']) #global-context-panel:not(.collapsed) .panel-title"), + '', + ); +}); diff --git a/src/data/cctv.js b/src/data/cctv.js index da65f5f08..ad95f2794 100644 --- a/src/data/cctv.js +++ b/src/data/cctv.js @@ -47,6 +47,8 @@ export const prioritizeActiveCctvGeometryRecord = export const processGeometryBatch = layer.processGeometryBatch; export const hideCctvRecordVisuals = layer.hideCctvRecordVisuals; export const refreshCoverageStyles = layer.refreshCoverageStyles; +export const refreshHorizonCulling = layer.refreshHorizonCulling; +export const _viewRecordIdsForTest = layer._viewRecordIdsForTest; export const clearProbeClampOnDeactivation = layer.clearProbeClampOnDeactivation; export const cctvRecordNeedsActivation = layer.cctvRecordNeedsActivation; diff --git a/src/data/cctv.test.mjs b/src/data/cctv.test.mjs index dafaeb5f6..0b7cee20f 100644 --- a/src/data/cctv.test.mjs +++ b/src/data/cctv.test.mjs @@ -1309,11 +1309,12 @@ test('CCTV null-active coverage, auto-hop, cycling, and panel targets stay hones assert.ok(renderer, '_renderCctvState is missing'); assert.match( renderer[0], - /else if \(!activeId\)[\s\S]*?selectedIndex = -1/, + // Nothing active clears the pick; only the outside-view note may show. + /else if \(!activeId\)[\s\S]*?selectedIndex =[^;]*\? 0 : -1;/, ); assert.match( renderer[0], - /_cctvFocusBtn\.disabled\s*=\s*!enabled\s*\|\|\s*cameras\.length === 0\s*\|\|\s*!activeId/, + /_cctvFocusBtn\.disabled\s*=\s*!enabled\s*\|\|\s*catalogCount === 0\s*\|\|\s*!activeId/, ); } finally { cctvLayer.setParams({ autoHop: false }); diff --git a/src/data/cctvA22.test.mjs b/src/data/cctvA22.test.mjs new file mode 100644 index 000000000..b9ac24b20 --- /dev/null +++ b/src/data/cctvA22.test.mjs @@ -0,0 +1,226 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; + +import { + a22WebcamToSource, + loadA22SourcesFromOpenDataHub, +} from '../../server/providers/cctv/sources.js'; +import { + A22_DEFAULT_GROUND_ELEVATION_M, + A22_IMAGE_ORIGIN, + DEFAULT_A22_CCTV_URL, +} from '../../server/providers/cctv/constants.js'; +import { createCctvCatalog } from '../../server/providers/cctv/catalog.js'; +import { fetchWithinHost } from '../../server/providers/cctv/media.js'; +import { staticFrameRefreshMs } from './cctvLod.js'; + +/** One Open Data Hub `WebcamInfo` item, shaped like the live a22 payload. */ +const item = ( + patch = {}, + { km = '68', lat = 46.53591, lon = 11.49772 } = {}, +) => ({ + Id: `A22_${km}`, + Active: true, + Source: 'a22', + Shortname: 'ADS Sciliar', + Webcamname: { it: 'ADS Sciliar' }, + Webcamurl: `https://www.autobrennero.it/WebCamImg/km${km}.jpg`, + Mapping: { a22: { km } }, + GpsInfo: [{ Gpstype: 'position', Latitude: lat, Longitude: lon }], + LicenseInfo: { + ClosedData: false, + LicenseHolder: 'https://www.autobrennero.it/', + }, + ...patch, +}); + +test('an A22 webcam maps to a source on the pinned operator frame', () => { + const source = a22WebcamToSource(item()); + assert.equal(source.id, 'it-a22-km68'); + assert.equal(source.name, 'A22 ADS Sciliar (km 68)'); + assert.equal(source.city, 'A22 Brennero'); + assert.equal(source.cityId, 'a22'); + assert.equal(source.provider, 'Autostrada del Brennero'); + assert.equal(source.lat, 46.53591); + assert.equal(source.lon, 11.49772); + assert.equal(source.headingConfidence, 'low'); + assert.equal(source.feedType, 'image'); + assert.equal(source.sourceKind, 'a22-opendatahub'); + assert.equal(source.code, 'ADS SCILIAR'); + assert.equal(source.url, `${A22_IMAGE_ORIGIN}km68.jpg`); + assert.equal(source.snapshotUrl, source.url); +}); + +test('ground elevation follows the motorway from the Brenner Pass to the Po', () => { + const brenner = a22WebcamToSource( + item({ Shortname: 'Brennero' }, { km: '1', lat: 46.99682, lon: 11.50218 }), + ); + const ponte = a22WebcamToSource( + item( + { Shortname: 'Ponte Po' }, + { km: '272', lat: 45.03145, lon: 10.85369 }, + ), + ); + assert.ok(brenner.groundElevationM > 1300); + assert.ok(ponte.groundElevationM < 50); + // A kilometre the table does not know falls back to the corridor prior. + const unknown = a22WebcamToSource( + item({}, { km: '150', lat: 45.95, lon: 11.05 }), + ); + assert.equal(unknown.groundElevationM, A22_DEFAULT_GROUND_ELEVATION_M); +}); + +test('inactive cameras, off-host frames and bad geometry are dropped', () => { + assert.equal(a22WebcamToSource(item({ Active: false })), null); + assert.equal(a22WebcamToSource(item({ Source: 'other' })), null); + assert.equal( + a22WebcamToSource( + item({ Webcamurl: 'https://evil.test/WebCamImg/km68.jpg' }), + ), + null, + ); + // The frame must be exactly the camera's own kilometre image. + assert.equal( + a22WebcamToSource( + item({ Webcamurl: 'https://www.autobrennero.it/WebCamImg/km1.jpg' }), + ), + null, + ); + assert.equal( + a22WebcamToSource(item({ Mapping: { a22: { km: '../x' } } })), + null, + ); + // Milan: plausible, but off the Brenner–Modena corridor. + assert.equal( + a22WebcamToSource(item({}, { km: '68', lat: 45.46, lon: 9.19 })), + null, + ); + assert.equal(a22WebcamToSource(item({ GpsInfo: [] })), null); + assert.equal(a22WebcamToSource(null), null); +}); + +test('the loader dedupes, orders north to south, and degrades to empty', async (t) => { + t.mock.method(console, 'log', () => {}); + t.mock.method(console, 'warn', () => {}); + const requested = []; + const fetchMock = t.mock.method(globalThis, 'fetch', async (url) => { + requested.push(String(url)); + return Response.json({ + TotalResults: 3, + Items: [ + item( + { Shortname: 'Ponte Po' }, + { km: '272', lat: 45.03145, lon: 10.85369 }, + ), + item(), + item(), + item( + { Shortname: 'Brennero' }, + { km: '1', lat: 46.99682, lon: 11.50218 }, + ), + ], + }); + }); + const cameras = await loadA22SourcesFromOpenDataHub(); + assert.deepEqual(requested, [DEFAULT_A22_CCTV_URL]); + assert.deepEqual( + cameras.map((c) => c.id), + ['it-a22-km1', 'it-a22-km68', 'it-a22-km272'], + ); + + fetchMock.mock.mockImplementation( + async () => new Response('nope', { status: 503 }), + ); + assert.deepEqual(await loadA22SourcesFromOpenDataHub(), []); + fetchMock.mock.mockImplementation( + async () => + new Response(null, { + status: 302, + headers: { Location: 'https://evil.test/' }, + }), + ); + assert.deepEqual(await loadA22SourcesFromOpenDataHub(), []); + fetchMock.mock.mockImplementation(async () => { + throw new Error('offline'); + }); + assert.deepEqual(await loadA22SourcesFromOpenDataHub(), []); +}); + +const runCatalog = async (t) => { + const requested = []; + t.mock.method(console, 'log', () => {}); + t.mock.method(console, 'warn', () => {}); + t.mock.method(globalThis, 'fetch', async (url) => { + const href = String(url); + requested.push(href); + if (href === DEFAULT_A22_CCTV_URL) { + return Response.json({ Items: [item()] }); + } + return Response.json([]); + }); + const sources = await createCctvCatalog({ sourceRoot: '/nonexistent' })(); + return { requested, sources }; +}; + +const withEnv = async (patch, fn) => { + const saved = { ...process.env }; + try { + delete process.env.CCTV_SOURCES_FILE; + delete process.env.CCTV_SOURCES_JSON; + delete process.env.CCTV_A22_ENABLED; + Object.assign(process.env, patch); + await fn(); + } finally { + for (const key of Object.keys(process.env)) { + if (!(key in saved)) delete process.env[key]; + } + Object.assign(process.env, saved); + } +}; + +test('the A22 lane is wired into the catalog', async (t) => { + await withEnv({}, async () => { + const { requested, sources } = await runCatalog(t); + assert.ok(requested.includes(DEFAULT_A22_CCTV_URL)); + assert.deepEqual( + sources.filter((s) => s.cityId === 'a22').map((s) => s.id), + ['it-a22-km68'], + ); + }); +}); + +test('CCTV_A22_ENABLED=0 keeps the lane from being loaded', async (t) => { + await withEnv({ CCTV_A22_ENABLED: '0' }, async () => { + const { requested, sources } = await runCatalog(t); + assert.equal(requested.includes(DEFAULT_A22_CCTV_URL), false); + assert.deepEqual( + sources.filter((s) => s.cityId === 'a22'), + [], + ); + }); +}); + +test('a redirect on the A22 frame host stays inside WebCamImg/', async () => { + const frame = `${A22_IMAGE_ORIGIN}km68.jpg`; + const redirectTo = (location) => async (url) => + url === frame + ? new Response(null, { status: 302, headers: { location } }) + : new Response('frame', { headers: { 'content-type': 'image/jpeg' } }); + for (const location of ['/it/area-riservata/', '/WebCamImg/../x.jpg']) + assert.equal( + await fetchWithinHost(frame, {}, redirectTo(location)), + null, + location, + ); + const followed = await fetchWithinHost( + frame, + {}, + redirectTo('/WebCamImg/km77.jpg'), + ); + assert.equal(await followed.text(), 'frame'); +}); + +test('A22 ambient cards refresh on the highway cadence, not the default', () => { + const source = a22WebcamToSource(item()); + assert.equal(staticFrameRefreshMs(source), 3 * 60 * 1000); +}); diff --git a/src/data/cctvLod.js b/src/data/cctvLod.js index 834dd964f..e1d54ae62 100644 --- a/src/data/cctvLod.js +++ b/src/data/cctvLod.js @@ -42,6 +42,9 @@ const PROVIDER_STATIC_REFRESH_MS = Object.freeze({ // every 20s showed no change inside 64s but a new frame by ~90-150s later — // matches the other highway packs' 3-minute cadence. 'servei català de trànsit': 3 * 60 * 1000, + // Autobrennero (A22) refreshes its stills about once a minute (Last-Modified + // tracks the minute); 3 min matches the other highway packs. + 'autostrada del brennero': 3 * 60 * 1000, }); /** diff --git a/src/data/cctvViewThinning.test.mjs b/src/data/cctvViewThinning.test.mjs new file mode 100644 index 000000000..1bc94eed8 --- /dev/null +++ b/src/data/cctvViewThinning.test.mjs @@ -0,0 +1,251 @@ +// src/data/cctvViewThinning.test.mjs — the CCTV view pass: only cameras on or +// near the screen show, a zoomed-out view keeps one camera per world-anchored +// cell, and the layer's UI state never rebuilds every camera for a listener +// that does not read the list. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import * as Cesium from 'cesium'; +import cctvLayer, { + _setCctvCoverageStateForTest, + _viewRecordIdsForTest, + refreshHorizonCulling, +} from './cctv.js'; + +const WIDTH = 800; +const HEIGHT = 600; + +/** + * Records at the given [lon, lat] degrees near the origin, each drawn at the + * screen point `screenAt` gives it (default: the middle of the screen). + */ +function recordsAt( + lonLats, + screenAt = () => ({ x: WIDTH / 2, y: HEIGHT / 2 }), +) { + const screenByPosition = new Map(); + const records = lonLats.map(([lon, lat], index) => { + const position = Cesium.Cartesian3.fromDegrees(lon, lat, 10); + screenByPosition.set(position, screenAt(index)); + return { + camera: { + id: `cam-${index}`, + name: `Camera ${index}`, + city: 'Test', + lat, + lon, + headingDeg: 0, + pitchDeg: -18, + fovDeg: 44, + rangeM: 145, + mountHeightM: 8, + }, + position, + billboard: { position, show: true }, + coverageEntities: [], + projection: null, + }; + }); + return { records, screenByPosition }; +} + +function viewerAt(heightM, screenByPosition, shiftX = 0) { + return { + isDestroyed: () => false, + camera: { + positionWC: Cesium.Cartesian3.fromDegrees(0, 0, heightM), + positionCartographic: { latitude: 0, longitude: 0, height: heightM }, + frustum: { fovy: Math.PI / 3 }, + }, + scene: { + canvas: { clientWidth: WIDTH, clientHeight: HEIGHT }, + cartesianToCanvasCoordinates: (position) => { + const screen = screenByPosition.get(position); + return screen && { x: screen.x + shiftX, y: screen.y }; + }, + }, + }; +} + +function shownIds(records) { + return records + .filter((record) => record.billboard.show) + .map((record) => record.camera.id) + .sort(); +} + +// Three cameras within ~110 m, and one ~22 km away. +const CLUSTER = [ + [0, 0], + [0.001, 0], + [0.0005, 0.0005], + [0.2, 0], +]; + +test('zoomed out, cameras sharing a ground cell show one icon', () => { + const { records, screenByPosition } = recordsAt(CLUSTER); + _setCctvCoverageStateForTest({ + viewer: viewerAt(20_000, screenByPosition), + records, + }); + assert.equal( + refreshHorizonCulling(), + true, + 'the first pass changes the view', + ); + const shown = shownIds(records); + assert.equal(shown.length, 2, 'one for the cluster, one for the far camera'); + assert.ok(shown.includes('cam-3'), 'a camera alone in its cell shows'); + assert.deepEqual(_viewRecordIdsForTest().sort(), shown); +}); + +test('a pan keeps the same winners, and an unchanged view keeps its list', () => { + const { records, screenByPosition } = recordsAt(CLUSTER, (index) => ({ + x: 100 + index * 7, + y: 300, + })); + _setCctvCoverageStateForTest({ + viewer: viewerAt(20_000, screenByPosition), + records, + }); + refreshHorizonCulling(); + const before = shownIds(records); + const list = _viewRecordIdsForTest(); + for (const shiftX of [3, 11, 20, 37]) { + _setCctvCoverageStateForTest({ + viewer: viewerAt(20_000, screenByPosition, shiftX), + records, + }); + refreshHorizonCulling(); + assert.deepEqual(shownIds(records), before, `pan by ${shiftX} px`); + } + assert.equal(refreshHorizonCulling(), false, 'the same view changes nothing'); + assert.deepEqual(_viewRecordIdsForTest(), list); +}); + +test('a small zoom within one cell size keeps the same winners', () => { + const { records, screenByPosition } = recordsAt(CLUSTER); + const shownAt = (heightM) => { + _setCctvCoverageStateForTest({ + viewer: viewerAt(heightM, screenByPosition), + records, + }); + refreshHorizonCulling(); + return shownIds(records); + }; + // Cells come in power-of-two metre steps: about 1.5 km and 1.6 km cells at + // these heights fall in the same step, so the same cameras win. + assert.deepEqual(shownAt(20_500), shownAt(20_000)); +}); + +test('a healthy camera wins its cell over an unknown one', () => { + const { records, screenByPosition } = recordsAt(CLUSTER.slice(0, 2)); + // Whichever camera the id order would pick, the healthy one must win. + for (const id of ['cam-0', 'cam-1']) { + _setCctvCoverageStateForTest({ + viewer: viewerAt(20_000, screenByPosition), + records, + healthById: new Map([[id, { status: 'ok' }]]), + }); + refreshHorizonCulling(); + assert.deepEqual(shownIds(records), [id]); + } +}); + +test('the active camera always shows, even in a crowded cell', () => { + const { records, screenByPosition } = recordsAt(CLUSTER.slice(0, 3)); + for (const active of records) { + _setCctvCoverageStateForTest({ + viewer: viewerAt(20_000, screenByPosition), + records, + activeCameraId: active.camera.id, + }); + refreshHorizonCulling(); + assert.ok(active.billboard.show, `${active.camera.id} shows while active`); + } +}); + +test('close up, every camera near the screen shows and far-off ones hide', () => { + const { records, screenByPosition } = recordsAt( + [ + [0, 0], + [0.0001, 0], + [0.0002, 0], + ], + (index) => + index === 2 ? { x: WIDTH * 3, y: HEIGHT / 2 } : { x: 100, y: 100 }, + ); + _setCctvCoverageStateForTest({ + viewer: viewerAt(300, screenByPosition), + records, + }); + refreshHorizonCulling(); + assert.deepEqual(shownIds(records), ['cam-0', 'cam-1']); +}); + +test('a disabled layer skips the view pass', () => { + const { records, screenByPosition } = recordsAt(CLUSTER); + _setCctvCoverageStateForTest({ + viewer: viewerAt(20_000, screenByPosition), + records, + enabled: false, + }); + assert.equal(refreshHorizonCulling(), false); + assert.deepEqual(_viewRecordIdsForTest(), []); +}); + +test('UI state builds the full camera list only when it is read', () => { + const { records, screenByPosition } = recordsAt(CLUSTER.slice(0, 2)); + _setCctvCoverageStateForTest({ + viewer: viewerAt(300, screenByPosition), + records, + }); + refreshHorizonCulling(); + let built = 0; + for (const record of records) { + const camera = record.camera; + record.camera = new Proxy(camera, { + get(target, key) { + if (key === 'calibration') built += 1; + return target[key]; + }, + }); + } + const first = cctvLayer.getUIState(); + assert.equal(built, 0, 'listeners that skip `cameras` cost nothing'); + assert.deepEqual( + first.cameraIndex.map((camera) => camera.id), + ['cam-0', 'cam-1'], + ); + assert.equal( + cctvLayer.getUIState().cameraIndex, + first.cameraIndex, + 'the index keeps its identity while the view does', + ); + assert.equal(first.cameras.length, 2); + assert.ok(built > 0, 'reading `cameras` builds it'); + assert.equal(first.cameras, first.cameras, 'and builds it once'); +}); + +test('the camera picker lists the view, with the active camera first', () => { + const { records, screenByPosition } = recordsAt( + [ + [0, 0], + [0.0001, 0], + [0.0002, 0], + ], + (index) => (index === 1 ? { x: WIDTH * 3, y: 0 } : { x: 100, y: 100 }), + ); + _setCctvCoverageStateForTest({ + viewer: viewerAt(300, screenByPosition), + records, + activeCameraId: 'cam-1', + }); + refreshHorizonCulling(); + const state = cctvLayer.getUIState(); + assert.deepEqual( + state.cameraIndex.map((camera) => camera.id), + ['cam-1', 'cam-0', 'cam-2'], + 'the off-screen active camera leads, then what is in view', + ); + assert.equal(state.cameras.length, 3, 'the full list is still every camera'); +}); diff --git a/src/data/dataCredits.js b/src/data/dataCredits.js index b89958041..1fa28b0ba 100644 --- a/src/data/dataCredits.js +++ b/src/data/dataCredits.js @@ -343,6 +343,13 @@ export const DATA_CREDITS = [ key: 'panoramax', html: 'Street Level: imagery © Panoramax contributors, licence per picture', }, + { + key: 'a22-cctv', + html: + 'Motorway webcams (A22 Brennero–Modena): images © ' + + 'Autostrada del Brennero S.p.A., ' + + 'camera list via the Open Data Hub (NOI Techpark)', + }, { key: 'gbfs', html: diff --git a/src/data/directions.test.mjs b/src/data/directions.test.mjs index bf6e2878e..0522226d2 100644 --- a/src/data/directions.test.mjs +++ b/src/data/directions.test.mjs @@ -875,6 +875,13 @@ test('route numbers follow the page language and never break from their unit', ( ); const list = directionsStepList({ route }); assert.equal(list.items[1].lead, '240\u00a0м'); + + // Japanese unit words follow the number: 1時間5分, but 1.5 km. + setLocale('ja'); + assert.equal(routeDistanceText(1500), '1.5\u00a0km'); + assert.equal(routeDurationText(39), '39秒'); + assert.equal(routeDurationText(3900), '1時間5分'); + assert.equal(routeDurationText(7200), '2時間'); }); test('ramp and lane wording stays localized after choosing the maneuver form', (t) => { @@ -892,6 +899,12 @@ test('ramp and lane wording stays localized after choosing the maneuver form', ( 'Toma la salida bruscamente a la derecha', 'Toma el carril izquierdo', ], + [ + 'ja', + 'ランプを斜め左に進んでI-95に入る', + '出口を急角度で右に出る', + '左車線を通行', + ], [ 'zh-CN', '向左前方驶入匝道,进入 I-95', @@ -919,3 +932,19 @@ test('ramp and lane wording stays localized after choosing the maneuver form', ( ); } }); + +test('the reroute tooltip keeps German mode nouns capitalized', (t) => { + t.after(() => setLocale('en')); + const title = (id) => + directionsRowControls(idle).chips.find((chip) => chip.id === id).title; + assert.equal(title('mode-foot'), 'Walk — reroute for walk'); + setLocale('de'); + assert.equal( + title('mode-foot'), + 'Zu Fuß – Route im Modus „Zu Fuß“ neu berechnen', + ); + assert.equal( + title('mode-bike'), + 'Fahrrad – Route im Modus „Fahrrad“ neu berechnen', + ); +}); diff --git a/src/data/firmsProxy.test.mjs b/src/data/firmsProxy.test.mjs index a54bd42fe..9ed35ba24 100644 --- a/src/data/firmsProxy.test.mjs +++ b/src/data/firmsProxy.test.mjs @@ -1,7 +1,18 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import test from 'node:test'; +import { EventEmitter } from 'node:events'; +import dns from 'node:dns/promises'; +import https from 'node:https'; +import net from 'node:net'; +import { Readable } from 'node:stream'; import { filterTrailing24h, parseFirmsCsv } from './firmsCsv.js'; +import { + describeFetchError, + fetchFirms, + fetchIpv4, + isAddressSelectionFailure, +} from '../../server/providers/firms.js'; const config = fs.readFileSync(new URL('../../server/providers/firms.js', import.meta.url), 'utf8'); const start = config.indexOf(' async function refreshUpstream(key) {'); @@ -17,8 +28,8 @@ const recent = { acqDate: '2026-09-11', acqTime: '1100' }; // Inject only its upstream, clock and filter dependencies; keep its aggregation // and source-status code intact, including failures while consuming records. function createRefresh(fetchSource, filter = filterTrailing24h) { - return new Function('SOURCES', 'fetchSource', 'filterTrailing24h', 'Date', 'console', - `return (${refreshSource});`)(SOURCES, fetchSource, filter, { now: () => NOW }, { warn() {} }); + return new Function('SOURCES', 'fetchSource', 'filterTrailing24h', 'describeFetchError', 'Date', 'console', + `return (${refreshSource});`)(SOURCES, fetchSource, filter, describeFetchError, { now: () => NOW }, { warn() {} }); } test('FIRMS retains large sources in order and filters expired rows', async () => { @@ -78,3 +89,216 @@ test('FIRMS distinguishes all-source failure from successful empty sources', asy assert.deepEqual(result.fires, []); assert.deepEqual(result.sources, SOURCES.map(source => ({ source, count: 0, ok: true }))); }); + +// #68: Node's network-family auto-selection gives each address a short attempt +// timeout, abandons a slow IPv4 handshake for an unreachable IPv6 address, and +// reports only `fetch failed`. This is the shape Node 24 throws for it. +function addressSelectionFailure() { + const v4 = Object.assign(new Error('connect ETIMEDOUT 198.118.194.34:443'), { code: 'ETIMEDOUT' }); + const v6 = Object.assign(new Error('connect ENETUNREACH 2001:4d0:241a:40c0::34:443'), { code: 'ENETUNREACH' }); + const cause = Object.assign(new AggregateError([v4, v6]), { code: 'ETIMEDOUT' }); + return new TypeError('fetch failed', { cause }); +} +// Node 26's fetch wraps the same AggregateError in a ConnectTimeoutError. +function wrappedAddressSelectionFailure() { + const inner = addressSelectionFailure().cause; + const cause = Object.assign(new Error('Connect Timeout Error (attempted addresses: 198.118.194.34:443, 2001:4d0:241a:40c0::34:443, timeout: 10000ms)'), + { name: 'ConnectTimeoutError', code: 'UND_ERR_CONNECT_TIMEOUT', cause: inner }); + return new TypeError('fetch failed', { cause }); +} +const URL_WITH_KEY = 'https://firms.modaps.eosdis.nasa.gov/api/area/csv/SECRET/VIIRS_SNPP_NRT/world/2'; + +test('FIRMS failures log the socket cause, never the keyed URL', () => { + const described = describeFetchError(addressSelectionFailure()); + assert.equal(described, + 'fetch failed (connect ETIMEDOUT 198.118.194.34:443; connect ENETUNREACH 2001:4d0:241a:40c0::34:443)'); + assert.ok(!described.includes('SECRET')); + assert.equal(describeFetchError(wrappedAddressSelectionFailure()), described, 'Node 26 reports the same attempts'); + const single = new TypeError('fetch failed', { cause: Object.assign(new Error('getaddrinfo ENOTFOUND x'), { code: 'ENOTFOUND' }) }); + assert.equal(describeFetchError(single), 'fetch failed (getaddrinfo ENOTFOUND x)'); + assert.equal(describeFetchError(new Error('HTTP 503')), 'HTTP 503'); +}); + +test('FIRMS retries once over IPv4 when every address fails to connect', async () => { + for (const shape of [addressSelectionFailure, wrappedAddressSelectionFailure]) { + const retried = []; + await fetchFirms(URL_WITH_KEY, {}, { + fetchImpl: async () => { throw shape(); }, + fetchIpv4Impl: async (url) => { retried.push(url); return new Response('csv'); }, + warn() {}, + }); + assert.deepEqual(retried, [URL_WITH_KEY], `${shape.name} is retried`); + } + const signal = new AbortController().signal; + const ok = new Response('csv'); + const retried = []; + const warnings = []; + const response = await fetchFirms(URL_WITH_KEY, { signal }, { + fetchImpl: async () => { throw addressSelectionFailure(); }, + fetchIpv4Impl: async (url, options) => { retried.push([url, options]); return ok; }, + warn: (message) => warnings.push(message), + }); + assert.equal(response, ok); + assert.deepEqual(retried, [[URL_WITH_KEY, { signal }]], 'one retry, same URL and deadline'); + assert.equal(warnings.length, 1); + assert.match(warnings[0], /ETIMEDOUT 198\.118\.194\.34:443; .*ENETUNREACH .* retrying over IPv4/); + assert.ok(!warnings[0].includes('SECRET')); + + const failure = new Error('connect ETIMEDOUT 198.118.194.34:443'); + await assert.rejects(fetchFirms(URL_WITH_KEY, {}, { + fetchImpl: async () => { throw addressSelectionFailure(); }, + fetchIpv4Impl: async () => { throw failure; }, + warn() {}, + }), (err) => err === failure, 'a failed retry reports its own error'); +}); + +test('FIRMS does not retry anything but an address-selection failure', async () => { + const deps = (fetchImpl) => ({ + fetchImpl, + fetchIpv4Impl: async () => assert.fail('must not retry'), + warn: () => assert.fail('must not warn'), + }); + const serverError = new Response('down', { status: 503 }); + assert.equal(await fetchFirms(URL_WITH_KEY, {}, deps(async () => serverError)), serverError); + for (const thrown of [ + new TypeError('fetch failed', { cause: Object.assign(new Error('getaddrinfo ENOTFOUND x'), { code: 'ENOTFOUND' }) }), + new TypeError('fetch failed', { cause: new AggregateError([]) }), + new TypeError('fetch failed'), + new DOMException('The operation was aborted due to timeout', 'TimeoutError'), + ]) { + await assert.rejects(fetchFirms(URL_WITH_KEY, {}, deps(async () => { throw thrown; })), (err) => err === thrown); + } + const controller = new AbortController(); + controller.abort(); + const aborted = addressSelectionFailure(); + await assert.rejects(fetchFirms(URL_WITH_KEY, { signal: controller.signal }, + deps(async () => { throw aborted; })), (err) => err === aborted, 'an expired deadline is not retried'); +}); + +test('the IPv4 retry connects to an IPv4 address only', async (t) => { + // `localhost` resolves to both ::1 and 127.0.0.1. Listen on both with the + // same port and record which one the retry reaches; the TLS handshake then + // fails against a plain socket, which is fine — only the connection matters. + const reached = []; + const listen = (host, port) => new Promise((resolve, reject) => { + const server = net.createServer((socket) => { reached.push(host); socket.destroy(); }); + server.once('error', reject).listen(port, host, () => resolve(server)); + }); + const v4 = await listen('127.0.0.1', 0); + t.after(() => v4.close()); + const { port } = v4.address(); + let v6; + try { + v6 = await listen('::1', port); + } catch { + t.skip('no IPv6 loopback on this host, so IPv4-only cannot be told apart'); + return; + } + t.after(() => v6.close()); + const order = await dns.lookup('localhost', { all: true }); + if (order[0]?.family !== 6) { + t.skip('localhost resolves IPv4 first here, so IPv4-only cannot be told apart'); + return; + } + await assert.rejects(fetchIpv4(`https://localhost:${port}/`, { signal: AbortSignal.timeout(5_000) })); + assert.deepEqual(reached, ['127.0.0.1']); +}); + +// Answer the IPv4 retry with `status`, invoking the response callback +// asynchronously as `https.request` does, outside the caller's promise. +// The returned promise rejects on an uncaught exception, so a regression fails +// the race at once instead of crashing the test runner. +function mockIpv4Status(t, status, statusMessage = '') { + let crash; + const crashed = new Promise((_resolve, reject) => (crash = reject)); + const onUncaught = (error) => + crash(new Error(`uncaught exception: ${error.message}`)); + process.on('uncaughtException', onUncaught); + t.after(() => process.off('uncaughtException', onUncaught)); + t.mock.method(https, 'request', (_url, _options, callback) => { + const request = new EventEmitter(); + request.end = () => + setImmediate(() => + callback( + Object.assign(Readable.from([]), { + statusCode: status, + statusMessage, + headers: { 'content-type': 'text/csv' }, + }), + ), + ); + return request; + }); + return crashed; +} + +for (const status of [204, 205, 304]) { + test(`the IPv4 retry answering null-body status ${status} resolves instead of crashing`, async (t) => { + const crashed = mockIpv4Status(t, status); + const response = await Promise.race([fetchIpv4(URL_WITH_KEY), crashed]); + assert.equal(response.status, status); + assert.equal(response.body, null); + }); +} + +for (const status of [600, 999]) { + test(`the IPv4 retry answering out-of-range status ${status} rejects instead of crashing`, async (t) => { + const crashed = mockIpv4Status(t, status); + await assert.rejects( + Promise.race([fetchIpv4(URL_WITH_KEY), crashed]), + new RegExp(`unsupported status ${status}`), + ); + }); +} + +test('the IPv4 retry answering an invalid reason phrase rejects instead of crashing', async (t) => { + const crashed = mockIpv4Status(t, 200, 'OK\x7f'); + await assert.rejects(Promise.race([fetchIpv4(URL_WITH_KEY), crashed]), TypeError); +}); + +// The fixtures above copy Node's error shape; check it against a real fetch so +// a Node or undici change that stops matching fails here instead of silently +// disabling the retry. A closed port on `localhost` refuses both addresses. +test('a real all-addresses connect failure is recognised as address selection', async (t) => { + const probe = net.createServer(); + try { + await new Promise((resolve, reject) => probe.once('error', reject).listen(0, '::1', resolve)); + } catch { + t.skip('no IPv6 loopback on this host'); + return; + } + const { port } = probe.address(); + await new Promise((resolve) => probe.close(resolve)); + const v4 = net.createServer(); + await new Promise((resolve, reject) => v4.once('error', reject).listen(port, '127.0.0.1', resolve)); + await new Promise((resolve) => v4.close(resolve)); + const err = await fetch(`http://localhost:${port}/`).then(() => null, (error) => error); + assert.ok(err, 'a closed port refuses the connection'); + assert.equal(isAddressSelectionFailure(err), true, describeFetchError(err)); + assert.match(describeFetchError(err), /ECONNREFUSED .*; .*ECONNREFUSED/); +}); + +// #68 only stays fixed while both upstream calls go through fetchFirms; the +// refresh harness injects fetchSource, so guard the call sites directly. +test('every FIRMS upstream request goes through the IPv4 fallback', async () => { + assert.doesNotMatch(config, /await fetch\(/, 'no FIRMS request bypasses fetchFirms'); + const fetchStart = config.indexOf(' async function fetchSource(key, source) {'); + assert.notEqual(fetchStart, -1, 'FIRMS fetchSource exists'); + const fetchSourceText = config.slice(fetchStart, config.indexOf('\n }', fetchStart) + 4); + const calls = []; + const response = new Response('latitude,longitude\n'); + const fetchSource = new Function('fetchFirms', 'readResponseTextCapped', 'parseFirmsCsv', + 'FIRMS_MAX_RESPONSE_BYTES', 'AbortSignal', 'fetch', `return (${fetchSourceText});`)( + async (url, options) => { calls.push(['fetchFirms', url, options.signal]); return response; }, + async (res, cap, signal) => { calls.push(['read', res, signal]); return 'csv'; }, + () => [], + 1024, + AbortSignal, + () => assert.fail('fetchSource must call fetchFirms, not fetch'), + ); + assert.deepEqual(await fetchSource('SECRET', 'MODIS_NRT'), []); + assert.equal(calls.length, 2); + assert.equal(calls[0][0], 'fetchFirms'); + assert.ok(calls[0][2] instanceof AbortSignal); + assert.deepEqual(calls[1], ['read', response, calls[0][2]], 'the body read shares the request deadline'); +}); diff --git a/src/firstRunExperience.test.mjs b/src/firstRunExperience.test.mjs index 6629536e2..ca0ca5c0a 100644 --- a/src/firstRunExperience.test.mjs +++ b/src/firstRunExperience.test.mjs @@ -374,24 +374,26 @@ test('an overlay with NO class to watch still disarms the launcher', () => { assert.match(covered, /\} catch \{\s*\n\s*return false;\s*\n\s*\}/); }); -test('one ESC does one thing — the radio disclosure stops the launcher outright', () => { +test('one ESC does one thing — a Cockpit utility disclosure stops later document listeners', () => { const ui = fs.readFileSync( new URL('./ui/radioBindings.js', import.meta.url), 'utf8', ); - // stopPropagation() does NOT stop later listeners on the SAME document, so the - // disclosure's earlier capture handler closed the disclosure and the launcher - // dismissed itself off the same key. The earlier listener is the only one that - // can stop the later one — and only the immediate form does it. - const radioEsc = ui.slice(ui.indexOf("event.key !== 'Escape' ||")); - const claim = radioEsc.slice(0, radioEsc.indexOf('setRadioDisclosure(false')); + // stopPropagation() does NOT stop later listeners on the SAME document. The + // Context Radio disclosure that once raced the launcher this way is gone, and + // Cockpit's utility disclosures never share the screen with the launcher, but + // they own the same contract: their capture handler is the only one that can + // keep a later document listener (Cockpit exit, the launcher) off the key, and + // only the immediate form does it. + const utilityEsc = ui.slice(ui.indexOf('if (!displayOpen && !radioOpen) return;')); + const claim = utilityEsc.slice(0, utilityEsc.indexOf('setCockpitDisclosure(kind, false')); assert.match(claim, /event\.preventDefault\(\);/); assert.match(claim, /event\.stopImmediatePropagation\(\);/); assert.doesNotMatch( claim, /event\.stopPropagation\(\);/, - 'the plain form leaves the launcher listening and is the defect itself', + 'the plain form leaves later document listeners acting on the same key', ); // Belt on the launcher side: a key another surface already marked is not ours, @@ -1022,14 +1024,10 @@ test('the voice TOOL SCHEMA matches the pinned contract — the mission mapping ); const hudLayout = legacyTools.find((tool) => tool.name === 'set_hud') .parameters.properties.layout; - assert.deepEqual(hudLayout.enum, [ - 'tactical', - 'operator', - 'minimal', - 'cyber', - ]); - // Cyber deliberately adds one layout; first-run missions still change no tools. - hudLayout.enum = hudLayout.enum.filter((layout) => layout !== 'cyber'); + assert.deepEqual(hudLayout.enum, ['tactical', 'minimal', 'cyber']); + // Cyber deliberately adds one layout and Operator was retired; first-run + // missions still change no tools. + hudLayout.enum = ['tactical', 'operator', 'minimal']; const cameraMotion = legacyTools.find((tool) => tool.name === 'move_camera') .parameters.properties.motion; assert.deepEqual(cameraMotion.enum, [ diff --git a/src/hud.js b/src/hud.js index 023d617dc..6eff65df7 100644 --- a/src/hud.js +++ b/src/hud.js @@ -9,7 +9,7 @@ import { applicationServices } from './services/application.js'; * real-time at configurable cadences. * * The HUD auto-activates when a military-style shader (NVG, FLIR, CRT) is - * selected and supports three layout variants: tactical, operator, minimal. + * selected and supports the layout variants: tactical, minimal, cyber. * * Color theming is driven by the active shader mode via CSS custom properties. */ @@ -287,12 +287,6 @@ export class IntelHUD { this._el.innerHTML = ` -
- TOP SECRET // SI-TK // NOFORN - ${this._missionId} - PAGE 1/1 -
-
┌
@@ -1062,7 +1056,7 @@ export class IntelHUD { * Switch the HUD layout variant. Falls back to `'tactical'` if the * name is unrecognized. Cyber also applies a coordinated skin to the full * application shell; leaving Cyber restores the standard shell tokens. - * @param {string} variantName - One of `'tactical'`, `'operator'`, `'minimal'`, `'cyber'`. + * @param {string} variantName - One of `'tactical'`, `'minimal'`, `'cyber'`. */ setVariant(variantName) { this._variant = normalizeHudLayout(variantName); diff --git a/src/hudAltitudeDatum.test.mjs b/src/hudAltitudeDatum.test.mjs index ca14569e5..18ed56f92 100644 --- a/src/hudAltitudeDatum.test.mjs +++ b/src/hudAltitudeDatum.test.mjs @@ -575,3 +575,25 @@ test('a summary interrupted while typing can retry unchanged context', async (t) env.restore(); } }); + +// ── Retired layouts ───────────────────────────────────────────────────────── +// +// Share links keep the retired Operator code, so old links still name it. The +// real HUD, not a caller, is what turns it into the default Tactical layout. + +test('a retired Operator layout falls back to Tactical in the live HUD', () => { + const env = installHudEnvironment(); + let hud; + try { + hud = new IntelHUD(env.viewer); + hud._el = { dataset: {} }; + hud.setVariant('minimal'); + assert.equal(hud.getVariant(), 'minimal'); + hud.setVariant('operator'); + assert.equal(hud.getVariant(), 'tactical'); + assert.equal(hud._el.dataset.variant, 'tactical'); + } finally { + hud?.destroy(); + env.restore(); + } +}); diff --git a/src/hudLayoutPolicy.js b/src/hudLayoutPolicy.js index 6b3e2287b..81a64b782 100644 --- a/src/hudLayoutPolicy.js +++ b/src/hudLayoutPolicy.js @@ -1,7 +1,2 @@ /** Browser-free layout vocabulary shared by UI and server-side voice tools. */ -export const HUD_LAYOUTS = Object.freeze([ - 'tactical', - 'operator', - 'minimal', - 'cyber', -]); +export const HUD_LAYOUTS = Object.freeze(['tactical', 'minimal', 'cyber']); diff --git a/src/hudLocalityFormat.js b/src/hudLocalityFormat.js index 697075a82..b9b8c3ea0 100644 --- a/src/hudLocalityFormat.js +++ b/src/hudLocalityFormat.js @@ -8,7 +8,10 @@ import { formatNumber, getLocale, t } from './i18n/index.js'; import { unitLabel } from './ui/unitLabels.js'; import { localizePlaceName } from './ui/placeNames.js'; -/** An upper-case HUD quantity such as `13KM` / `13КМ`, in the active language. */ +/** + * An upper-case HUD quantity such as `13KM` / `13КМ`, in the active language. + * Chinese and Japanese have no case and keep the catalog's `13 km`. + */ export function hudQuantity(value, unit, digits = 0) { const number = formatNumber(value, { useGrouping: false, @@ -16,7 +19,7 @@ export function hudQuantity(value, unit, digits = 0) { maximumFractionDigits: digits, }); const label = unitLabel(unit); - if (getLocale().startsWith('zh-')) + if (/^(zh|ja)(-|$)/.test(getLocale())) return label ? `${number} ${label}` : number; return `${number}${label.toUpperCase()}`; } diff --git a/src/i18n/i18n.test.mjs b/src/i18n/i18n.test.mjs index 8dd0429ff..68541e282 100644 --- a/src/i18n/i18n.test.mjs +++ b/src/i18n/i18n.test.mjs @@ -5,6 +5,7 @@ import { applyDocumentTranslations, formatDate, formatNumber, + formatRelativeTime, msg, persistLocaleAndReload, setLocale, @@ -36,11 +37,20 @@ test('normalizeLocale folds regional variants and rejects unshipped tags', () => assert.equal(normalizeLocale('uk'), 'uk'); assert.equal(normalizeLocale('uk-UA'), 'uk'); assert.equal(normalizeLocale('en_GB'), 'en'); + assert.equal(normalizeLocale('de'), 'de'); + assert.equal(normalizeLocale('de-AT'), 'de'); + assert.equal(normalizeLocale('de_CH'), 'de'); + assert.equal(normalizeLocale('fr'), 'fr'); + assert.equal(normalizeLocale('fr-CA'), 'fr'); + assert.equal(normalizeLocale('FR_be'), 'fr'); assert.equal(normalizeLocale('pt-BR'), 'pt'); assert.equal(normalizeLocale('pt_PT'), 'pt'); assert.equal(normalizeLocale('xx'), null); assert.equal(normalizeLocale('it-IT'), 'it'); assert.equal(normalizeLocale('it_CH'), 'it'); + assert.equal(normalizeLocale('ja'), 'ja'); + assert.equal(normalizeLocale('ja-JP'), 'ja'); + assert.equal(normalizeLocale('JA_jp'), 'ja'); // Explicit script tags take precedence over regional defaults. assert.equal(normalizeLocale('zh'), 'zh-CN'); assert.equal(normalizeLocale('zh-CN'), 'zh-CN'); @@ -74,7 +84,7 @@ test('resolveLocale: ?lang= wins, then the stored choice, then browser languages resolveLocale({ location: { search: '?lang=xx' }, storage: memoryStorage(), - languages: ['de-DE', 'ru-RU'], + languages: ['ko-KR', 'ru-RU'], }), 'ru', ); @@ -82,7 +92,15 @@ test('resolveLocale: ?lang= wins, then the stored choice, then browser languages resolveLocale({ location: { search: '' }, storage: memoryStorage(), - languages: ['de'], + languages: ['de-DE', 'ru-RU'], + }), + 'de', + ); + assert.equal( + resolveLocale({ + location: { search: '' }, + storage: memoryStorage(), + languages: ['ko'], }), 'en', ); @@ -200,7 +218,13 @@ test('Intl formatters follow the active locale', () => { assert.equal(formatNumber(1234.5), '1,234.5'); setLocale('ru'); assert.equal(formatDate(date, options), '15 января 2026 г.'); + assert.equal(formatRelativeTime(-5, 'minute'), '5 минут назад'); assert.equal(formatNumber(1234.5), '1 234,5'); + // Japanese keeps a number and its unit together, as the catalog does. + setLocale('ja'); + assert.equal(formatDate(date, options), '2026年1月15日'); + assert.equal(formatRelativeTime(-5, 'minute'), '5分前'); + assert.equal(formatRelativeTime(-3, 'day'), '3日前'); }); function fakeElement({ text = '', attributes = {} } = {}) { diff --git a/src/i18n/index.js b/src/i18n/index.js index 40fb6a16f..63dbe6cf2 100644 --- a/src/i18n/index.js +++ b/src/i18n/index.js @@ -16,8 +16,11 @@ import { normalizeLocale, writeStoredLocale, } from './locale.js'; +import de from './locales/de.json' with { type: 'json' }; import es from './locales/es.json' with { type: 'json' }; +import fr from './locales/fr.json' with { type: 'json' }; import it from './locales/it.json' with { type: 'json' }; +import ja from './locales/ja.json' with { type: 'json' }; import pt from './locales/pt.json' with { type: 'json' }; import ru from './locales/ru.json' with { type: 'json' }; import uk from './locales/uk.json' with { type: 'json' }; @@ -25,8 +28,11 @@ import zhCN from './locales/zh-CN.json' with { type: 'json' }; import zhHant from './locales/zh-Hant.json' with { type: 'json' }; const CATALOGS = Object.freeze({ + de, es, + fr, it, + ja, pt, ru, uk, @@ -212,13 +218,19 @@ export function formatUtcDayMonth(value) { /** * Locale-aware relative time (Intl.RelativeTimeFormat), e.g. - * formatRelativeTime(-5, 'minute') → '5 minutes ago' / '5 минут назад'. + * formatRelativeTime(-5, 'minute') → '5 minutes ago' / '5 минут назад' / + * '5分前'. */ export function formatRelativeTime(value, unit = 'second', options) { - return cachedFormatter('relative', Intl.RelativeTimeFormat, { + const text = cachedFormatter('relative', Intl.RelativeTimeFormat, { numeric: 'auto', ...options, }).format(value, unit); + // CLDR spaces Japanese (5 分前); the ja catalog writes the number and its + // unit together (5分前), so the two must not differ on one screen. + return currentLocale === 'ja' + ? text.replace(/(?<=\d) (?=\p{Script=Han})/gu, '') + : text; } /** Collapse markup whitespace so a message reads the same in HTML and JS. */ diff --git a/src/i18n/ja-display.test.mjs b/src/i18n/ja-display.test.mjs new file mode 100644 index 000000000..ade04f519 --- /dev/null +++ b/src/i18n/ja-display.test.mjs @@ -0,0 +1,21 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { setLocale } from './index.js'; +import { + cockpitUnitCase, + formatCockpitDistance, +} from '../ui/cockpitPresentation.js'; + +test('Japanese cockpit distances keep the unit in its own case', () => { + try { + setLocale('ja'); + assert.equal(formatCockpitDistance(14000), '14 km'); + assert.equal(formatCockpitDistance(9500), '9.5 km'); + assert.equal(cockpitUnitCase('km'), 'km'); + setLocale('en'); + assert.equal(formatCockpitDistance(14000), '14 KM'); + assert.equal(cockpitUnitCase('km'), 'KM'); + } finally { + setLocale('en'); + } +}); diff --git a/src/i18n/ja-hud-units.test.mjs b/src/i18n/ja-hud-units.test.mjs new file mode 100644 index 000000000..73568d0ce --- /dev/null +++ b/src/i18n/ja-hud-units.test.mjs @@ -0,0 +1,22 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { setLocale } from './index.js'; +import { hudQuantity, LOCALIZED_LOCALITY } from '../hudLocalityFormat.js'; +import { formatCockpitDistance } from '../ui/cockpitPresentation.js'; + +// revmain-opus-ja-i1-1: ja has no case, so HUD units keep the catalog case +// (km), as the cockpit already does and docs/translations/ja.md requires. +test('Japanese HUD units keep their case, like the cockpit', () => { + try { + setLocale('ja'); + assert.equal(formatCockpitDistance(14000), '14 km'); + assert.equal(hudQuantity(13, 'km'), '13 km'); + assert.equal(hudQuantity(1.5, 'km', 1), '1.5 km'); + assert.equal( + LOCALIZED_LOCALITY.near({ poi: 'Shibuya', city: 'Tokyo', distKm: 13 }), + 'SHIBUYA(東京)付近 13 km', + ); + } finally { + setLocale('en'); + } +}); diff --git a/src/i18n/locale.js b/src/i18n/locale.js index 0bdad9382..3297fcf88 100644 --- a/src/i18n/locale.js +++ b/src/i18n/locale.js @@ -10,8 +10,11 @@ export const DEFAULT_LOCALE = 'en'; /** Shipped locales; order does not matter. Adding one: docs/TRANSLATORS.md. */ export const LOCALES = Object.freeze([ 'en', + 'de', 'es', + 'fr', 'it', + 'ja', 'pt', 'ru', 'uk', @@ -22,8 +25,11 @@ export const LOCALES = Object.freeze([ /** Each locale named in its own language, for the selector. */ export const LOCALE_NAMES = Object.freeze({ en: 'English', + de: 'Deutsch', es: 'Español', + fr: 'Français', it: 'Italiano', + ja: '日本語', pt: 'Português (Brasil)', ru: 'Русский', uk: 'Українська', @@ -36,6 +42,7 @@ const RTL_LOCALES = Object.freeze([]); /** * Map any language tag to a shipped locale: 'ru-RU' and 'RU' → 'ru', + * 'de-AT' and 'de_CH' → 'de', 'ja-JP' → 'ja', * 'pt-BR' and 'pt-PT' → 'pt' (Brazilian Portuguese), * 'zh', 'zh-CN' and 'zh-Hans-SG' → 'zh-CN'; * 'zh-Hant', 'zh-TW', 'zh-HK' and 'zh-MO' → 'zh-Hant'. diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json new file mode 100644 index 000000000..8e383aded --- /dev/null +++ b/src/i18n/locales/de.json @@ -0,0 +1,2930 @@ +{ + "Flying to Austin, TX...": "Flug nach Austin, TX...", + "Restoring shared view...": "Geteilte Ansicht wird wiederhergestellt...", + "Configuring viewer...": "Viewer wird konfiguriert...", + "Loading Google 3D Tiles...": "Google 3D Tiles werden geladen...", + "Loading the keyless globe...": "Globus ohne Schlüssel wird geladen...", + "Google 3D Tiles unavailable ({detail}). Loading the keyless globe...": "Google 3D Tiles nicht verfügbar ({detail}). Globus ohne Schlüssel wird geladen...", + "Initializing systems...": "Systeme werden initialisiert...", + "Close data attribution": "Datenquellenhinweis schließen", + "UNAVAILABLE": "NICHT VERFÜGBAR", + "SOURCE": "QUELLE", + "Overpass rate-limited": "Overpass-Anfragelimit erreicht", + "Overpass timed out": "Zeitüberschreitung bei Overpass", + "Overpass could not complete the query": "Overpass konnte die Abfrage nicht abschließen", + "Overpass temporarily unavailable": "Overpass vorübergehend nicht verfügbar", + "Retrying mapped sites…": "Neuer Versuch für kartierte Standorte…", + "Fetching mapped sites…": "Kartierte Standorte werden abgerufen…", + "{reason} — retrying in {seconds}s": "{reason}: neuer Versuch in {seconds} s", + "{reason} — retry pending": "{reason}: neuer Versuch steht aus", + "Zoom in to search mapped installations": "Hineinzoomen, um kartierte Anlagen zu suchen", + "Showing cached mapped sites": "Kartierte Standorte aus dem Cache", + "Mapped sites not loaded": "Kartierte Standorte nicht geladen", + "Mapped sites loaded": "Kartierte Standorte geladen", + "feed unavailable": "Feed nicht verfügbar", + "feed stale": "Feed veraltet", + "observed or mapped nearby context": "beobachteter oder kartierter Kontext in der Nähe", + "no observed or mapped objects in current feeds": "keine beobachteten oder kartierten Objekte in den aktuellen Feeds", + "STATION": "STATION", + "Crewed stations and their visiting vehicles": "Bemannte Stationen und angedockte Raumfahrzeuge", + "NAV": "NAV", + "GNSS navigation — GPS, GLONASS, Galileo": "GNSS-Navigation: GPS, GLONASS, Galileo", + "GEO": "GEO", + "Geostationary belt — comms and weather, fixed over the equator": "Geostationärer Gürtel: Kommunikation und Wetter, fest über dem Äquator", + "VISUAL": "VISUELL", + "Brightest naked-eye objects — CelesTrak visual group": "Hellste mit bloßem Auge sichtbare Objekte: CelesTrak-Gruppe „visual“", + "COMMS": "KOMM.", + "Broadband constellation shell — shown only in DENSE mode": "Breitband-Konstellationsschale: nur im Modus DICHT sichtbar", + "FISHING": "FISCHEREI", + "TOWING": "SCHLEPPVERBAND", + "DREDGER": "BAGGER", + "DIVE OPS": "TAUCHEINSATZ", + "MILITARY": "MILITÄR", + "SAILING": "SEGLER", + "PLEASURE": "SPORTBOOT", + "PILOT": "LOTSENBOOT", + "SAR": "SAR", + "TUG": "SCHLEPPER", + "PORT TENDER": "HAFENTENDER", + "ANTI-POLLUTION": "UMWELTSCHUTZ", + "LAW ENFORCE": "POLIZEI", + "MEDICAL": "SANITÄTSDIENST", + "HIGH-SPEED": "SCHNELLSCHIFF", + "PASSENGER": "PASSAGIER", + "CARGO": "FRACHT", + "TANKER": "TANKER", + "OTHER": "SONSTIGE", + "ENVIRONMENTAL": "UMWELT", + "EARTH WATCH": "ERDBEOBACHTUNG", + "ACTIVE EVENTS": "AKTIVE EREIGNISSE", + "Starting live contacts…": "Live-Kontakte werden gestartet…", + "Opening space missions…": "Raummissionen werden geöffnet…", + "Scanning active events…": "Aktive Ereignisse werden gescannt…", + "Live earthquakes and active fires, from USGS and NASA": "Live-Erdbeben und aktive Brände, von USGS und NASA", + "It feels like a forbidden cockpit—then you realize the sources are public and the data is real.": "Es fühlt sich an wie ein verbotenes Cockpit – dann merkt man, dass die Quellen öffentlich und die Daten echt sind.", + "Working…": "In Arbeit…", + "Could not open that mission{detail}. Retry or explore manually.": "Mission{detail} konnte nicht geöffnet werden. Erneut versuchen oder manuell erkunden.", + "This browser is blocking storage, so that could not be saved.": "Dieser Browser blockiert den Speicher, daher konnte das nicht gespeichert werden.", + "CRT": "CRT", + "NVG": "NVG", + "FLIR": "FLIR", + "SNOW": "SCHNEE", + "LOADING": "LÄDT", + "DEGRADED": "EINGESCHRÄNKT", + "STALE": "VERALTET", + "PARTIAL": "TEILWEISE", + "FALLBACK": "FALLBACK", + "SUMMARY": "ÜBERSICHT", + "Awaiting telemetry...": "Warten auf Telemetrie...", + "REC": "REC", + "AIS: --": "AIS: --", + "POWER UP · {count} KEYS WAITING": { + "one": "STARTKLAR · {count} SCHLÜSSEL OFFEN", + "other": "STARTKLAR · {count} SCHLÜSSEL OFFEN" + }, + "POWERED UP": "HOCHGEFAHREN", + "Save failed ({status}).": "Speichern fehlgeschlagen ({status}).", + "REMOVE": "ENTFERNEN", + "Remove {title} from this app's saved keys": "{title} aus den gespeicherten Schlüsseln dieser App entfernen", + "your app configuration": "der App-Konfiguration", + "your local .env": "der lokalen .env", + "Saving…": "Speichern…", + "Save failed: {detail}": "Speichern fehlgeschlagen: {detail}", + "Paste at least one key first.": "Zuerst mindestens einen Schlüssel einfügen.", + "Saved to {store}. Restarting — this page reloads itself.": "In {store} gespeichert. Neustart läuft, diese Seite lädt sich selbst neu.", + "Remove this key from your saved configuration?": "Diesen Schlüssel aus der gespeicherten Konfiguration entfernen?", + "Removed from {store}. Restarting — this page reloads itself.": "Aus {store} entfernt. Neustart läuft, diese Seite lädt sich selbst neu.", + "Places context + Street View fallback; optional separate key": "Places-Kontext + Street-View-Fallback; optional eigener Schlüssel", + "Live ships, worldwide": "Live-Schiffe, weltweit", + "Live active-fire detections": "Live-Erkennung aktiver Brände", + "Real live traffic (keyless runs a simulation)": "Echter Live-Verkehr (ohne Schlüssel: Simulation)", + "Bing imagery map stacks + world terrain": "Bing-Bildmaterial als Kartenstapel + globales Geländemodell", + "More flight-polling credits (anonymous works without)": "Mehr Abfrage-Credits für Flüge (anonym geht es auch ohne)", + "CONTEXT READY": "KONTEXT BEREIT", + "GLOBAL CONTEXT OFF": "GLOBALER KONTEXT AUS", + "SELECT A FLIGHT, VESSEL, OR MAPPED INSTALLATION": "FLUG, SCHIFF ODER KARTIERTE ANLAGE WÄHLEN", + "ENABLE TO LOAD OBSERVED / MAPPED PROXIMITY": "AKTIVIEREN FÜR BEOBACHTETE / KARTIERTE OBJEKTE IM UMKREIS", + "Unavailable": "Nicht verfügbar", + "Focus {label}": "{label} fokussieren", + "Global Context navigation": "Navigation des globalen Kontexts", + "Previous — prior visited contact in the 250 km window": "Zurück: zuvor besuchter Kontakt im 250-km-Fenster", + "PREVIOUS": "ZURÜCK", + "FOCUS": "FOKUS", + "Next — nearest unvisited contact in the 250 km window": "Weiter: nächstgelegener unbesuchter Kontakt im 250-km-Fenster", + "NEXT": "WEITER", + "Open-source mapped/observed context. Missing broadcasts, unloaded map areas, or unmapped sites are not evidence of absence.": "Kontext aus offenen Quellen, kartiert/beobachtet. Fehlende Funksignale, nicht geladene Kartenbereiche oder nicht kartierte Standorte sind kein Beleg für Abwesenheit.", + "HDG {value}°": "HDG {value}°", + "BRG {value}°": "BRG {value}°", + "BRG —": "BRG —", + "CRS {value}°": "CRS {value}°", + "CURRENT VIEWPORT ONLY": "NUR AKTUELLER BILDAUSSCHNITT", + "WITHIN {km} KM": "INNERHALB {km} KM", + "Flights": "Flüge", + "Military flights": "Militärflüge", + "AIS vessels": "AIS-Schiffe", + "Mapped installations": "Kartierte Anlagen", + "syncing {count} city feeds...": "Synchronisierung der Stadt-Feeds ({count})...", + "scanning nearby systems...": "Systeme in der Nähe werden gesucht...", + "Station {id}": "Station {id}", + "Station": "Station", + "🚲 {bikes} avail · {docks} docks · {capacity} cap": "🚲 {bikes} frei · {docks} Docks · {capacity} Kap.", + "⚠️ Not installed": "⚠️ Nicht installiert", + "⚠️ Not renting": "⚠️ Keine Ausleihe", + "⚠️ Not returning": "⚠️ Keine Rückgabe", + "HDG {value}° (ESTIMATED)": "HDG {value}° (GESCHÄTZT)", + "{count} CAMERAS STANDING BY · NO CAMERA SELECTED · CLICK A CAMERA TO ACTIVATE": "KAMERAS BEREIT: {count} · KEINE KAMERA GEWÄHLT · KAMERA ANKLICKEN ZUM AKTIVIEREN", + "No cameras available in catalog.": "Keine Kameras im Katalog verfügbar.", + "{city} CCTV": "{city} CCTV", + "FOV {value}°": "FOV {value}°", + "COVERAGE {value}km²": "ABDECKUNG {value} km²", + "OVERLAP {count} cams": "ÜBERLAPPUNG {count} Kam.", + "ISOLATED VIEW": "EINZELANSICHT", + "PROJ MONITOR": "PROJ MONITOR", + "PROJ OFF": "PROJ AUS", + "VIEWSHED": "SICHTBEREICH", + "CAL {value}": "KAL. {value}", + "SRC {value}": "QUELLE {value}", + "{value} CONTEXT": "KONTEXT {value}", + "Focus fire detection {title}, {details}": "Branderkennung {title} fokussieren, {details}", + "{value} conf": "Konf. {value}", + "{value} ago": "vor {value}", + "sensor n/a": "Sensor k. A.", + "FIRE · {frp} MW": "BRAND · {frp} MW", + "NIGHT": "NACHT", + "▲ {frp} MW": "▲ {frp} MW", + "FIRE": "BRAND", + "FIRES": "BRÄNDE", + "max {value} MW": "max. {value} MW", + "new {value}": "zuletzt vor {value}", + "just now": "gerade eben", + "<1m ago": "vor <1 min", + "{count}m ago": "vor {count} min", + "{count}h ago": "vor {count} h", + "STALE · cached {age}": "VERALTET · im Cache {age}", + "refreshing...": "wird aktualisiert...", + "loading...": "lädt...", + "KEY REQUIRED": "SCHLÜSSEL ERFORDERLICH", + "LIVE · updated {age}": "LIVE · aktualisiert {age}", + "loading mapped installation context": "Kontext der kartierten Anlage wird geladen", + "MAPPED INSTALLATION": "KARTIERTE ANLAGE", + "LAUNCH SITE · {site}": "STARTPLATZ · {site}", + "LAUNCH SITE": "STARTPLATZ", + "T−{time} · {mission}": "T−{time} · {mission}", + "LAUNCH STANDBY": "STARTBEREITSCHAFT", + "LIFTOFF · {mission}": "ABHEBEN · {mission}", + "ASCENT REPLAY · {mission}": "AUFSTIEGS-REPLAY · {mission}", + "ASCENT ESTIMATE · {mission}": "AUFSTIEGSSCHÄTZUNG · {mission}", + "STAGE RE-ENTRY / RECOVERY · {mission}": "STUFEN-WIEDEREINTRITT / BERGUNG · {mission}", + "ORBIT REPLAY · {mission}": "ORBIT-REPLAY · {mission}", + "PAUSED · {title}": "PAUSIERT · {title}", + "UNSPECIFIED": "NICHT ANGEGEBEN", + "+{count} additional payload records": "Weitere Nutzlast-Einträge: +{count}", + "PAYLOAD DATA UNAVAILABLE": "NUTZLASTDATEN NICHT VERFÜGBAR", + "{value} KM DOWNRANGE": "{value} KM DOWNRANGE", + "POSITION UNAVAILABLE": "POSITION NICHT VERFÜGBAR", + "FLIGHT {count}": "FLUG {count}", + "REUSED": "WIEDERVERWENDET", + "NO STAGE RE-ENTRY / RECOVERY DATA": "KEINE DATEN ZU STUFEN-WIEDEREINTRITT / BERGUNG", + "{count} / 30D": "{count} / 30 T", + "NO MISSIONS AVAILABLE IN THE CURRENT 30-DAY WINDOW": "KEINE MISSIONEN IM AKTUELLEN 30-TAGE-FENSTER", + "DATE UNAVAILABLE": "DATUM NICHT VERFÜGBAR", + "UNSPECIFIED OPERATOR": "BETREIBER NICHT ANGEGEBEN", + "Select {mission}": "{mission} auswählen", + "{value} KM": "{value} KM", + "{value} KM/S": "{value} KM/S", + "SELECTED SPACE MISSION": "GEWÄHLTE RAUMMISSION", + "Show all missions": "Alle Missionen anzeigen", + "Deselect mission": "Mission abwählen", + "MISSION": "MISSION", + "STATUS · ": "STATUS · ", + "LAUNCH SITE · ": "STARTPLATZ · ", + "LAUNCH TIME · ": "STARTZEIT · ", + "ORBIT · ": "ORBIT · ", + "ASCENT PATH · ": "AUFSTIEGSPFAD · ", + "CURRENT DISTANCE FROM EARTH · ": "AKTUELLE ENTFERNUNG VON DER ERDE · ", + "SATELLITE SPEED · ": "SATELLITENGESCHW. · ", + "PAYLOAD": "NUTZLAST", + "NAME": "NAME", + "TYPE": "TYP", + "DESTINATION": "ZIEL", + "STAGE / RE-ENTRY / RECOVERY": "STUFE / WIEDEREINTRITT / BERGUNG", + "STAGE": "STUFE", + "STATUS": "STATUS", + "FINAL POSITION": "ENDPOSITION", + "REPLAY SPEED": "WIEDERGABETEMPO", + "Replay speed multiplier": "Faktor der Wiedergabegeschwindigkeit", + "REPLAY ASCENT": "AUFSTIEG ABSPIELEN", + "Previous mission": "Vorherige Mission", + "PREV": "ZURÜCK", + "Next mission": "Nächste Mission", + "SHOW ALL / DESELECT": "ALLE ZEIGEN / ABWÄHLEN", + "Pause replay": "Wiedergabe pausieren", + "Cancel replay": "Wiedergabe abbrechen", + "EST. DOWNRANGE": "DOWNRANGE GESCHÄTZT", + "PLANNED · ": "GEPLANT · ", + "SUPPLIED TRAJECTORY POINTS": "GELIEFERTE BAHNPUNKTE", + "RECONSTRUCTED ESTIMATE": "REKONSTRUIERTE SCHÄTZUNG", + "Replay the estimated ascent with a following camera": "Geschätzten Aufstieg mit einer mitfliegenden Kamera abspielen", + "Resume replay": "Wiedergabe fortsetzen", + "T minus {seconds}": "T minus {seconds}", + "Preparing launch site": "Startplatz wird vorbereitet", + "Liftoff": "Abheben", + "Ascent replay": "Aufstiegs-Replay", + "Orbit replay": "Orbit-Replay", + ", paused": ", pausiert", + "Alt unknown": "Höhe unbekannt", + "Type unknown": "Typ unbekannt", + "Reg unknown": "Kennz. unbekannt", + "Operator unknown": "Betreiber unbekannt", + "News": "Nachrichten", + "Talk": "Talk", + "Weather / Emergency": "Wetter / Notfall", + "Public Safety": "Öffentliche Sicherheit", + "Aviation / Marine": "Luftfahrt / Seefahrt", + "Traffic / Transit": "Verkehr / ÖPNV", + "Music": "Musik", + "Other": "Sonstige", + "radio|TALK": "TALK", + "radio|SAFETY": "SICHERH.", + "radio|AIR / SEA": "LUFT / SEE", + "radio|TRANSIT": "ÖPNV", + "radio|MUSIC": "MUSIK", + "Alternative": "Alternative", + "Ambient": "Ambient", + "Blues": "Blues", + "Classical": "Klassik", + "Country": "Country", + "Dance": "Dance", + "Electronic": "Elektronische Musik", + "Folk": "Folk", + "Funk": "Funk", + "Hip-Hop": "Hip-Hop", + "House": "House", + "Indie": "Indie", + "Jazz": "Jazz", + "Latin": "Latin", + "Metal": "Metal", + "Oldies": "Oldies", + "Pop": "Pop", + "Punk": "Punk", + "R&B": "R&B", + "Reggae": "Reggae", + "Rock": "Rock", + "Soul": "Soul", + "Techno": "Techno", + "Trance": "Trance", + "World": "Weltmusik", + "Add the full Starlink broadband shell (thousands of extra points)": "Komplette Starlink-Breitbandschale hinzufügen (Tausende zusätzliche Punkte)", + "Loading the Starlink shell…": "Starlink-Schale wird geladen…", + "Starlink {detail} — click to retry": "Starlink: {detail} – zum erneuten Versuch klicken", + "load failed": "Laden fehlgeschlagen", + "Showing the full Starlink shell — click for the core catalog only": "Komplette Starlink-Schale sichtbar: klicken, um nur den Kernkatalog zu zeigen", + "DENSE ···": "DICHT ···", + "DENSE ✕": "DICHT ✕", + "DENSE": "DICHT", + "Syncing flow · Roads: {source}": "Verkehrsfluss wird synchronisiert · Straßen: {source}", + "LIVE · Roads: {source} · Roads without flow hidden": "LIVE · Straßen: {source} · Straßen ohne Verkehrsdaten ausgeblendet", + "LIVE · Roads: {source} · No flow roads in view": "LIVE · Straßen: {source} · Keine Straßen mit Verkehrsdaten im Bild", + "LIVE · Roads: {source} · Flow {percent}%": "LIVE · Straßen: {source} · Live-Abdeckung {percent} %", + "SIMULATED · Roads: {source} · Flow {percent}%": "SIMULIERT · Straßen: {source} · Live-Abdeckung {percent} %", + "Syncing flow · Roads: OpenStreetMap · Flow: TomTom · Unmatched: simulated": "Verkehrsfluss wird synchronisiert · Straßen: OpenStreetMap · Verkehrsfluss: TomTom · Ohne Zuordnung: simuliert", + "LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov · Unmatched: simulated": "LIVE · Straßen: OpenStreetMap · Verkehrsfluss: TomTom · {percent} % Abd. · Ohne Zuordnung: simuliert", + "LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov": "LIVE · Straßen: OpenStreetMap · Verkehrsfluss: TomTom · {percent} % Abd.", + "SIMULATED · Roads: OpenStreetMap · Flow: TomTom (no matches)": "SIMULIERT · Straßen: OpenStreetMap · Verkehrsfluss: TomTom (keine Treffer)", + "SIMULATED — traffic service unreachable": "SIMULIERT: Verkehrsdienst nicht erreichbar", + "SIMULATED — add TomTom key for live": "SIMULIERT: TomTom-Schlüssel für Live-Daten hinzufügen", + "AIS: {name}": "AIS: {name}", + "{type} SPD: {speed} HDG: {heading}": "{type} SPD: {speed} HDG: {heading}", + "VESSEL": "SCHIFF", + "POS: LIVE": "POS: LIVE", + "POS: {time}Z": "POS: {time}Z", + "awaiting first AIS position…": "warten auf erste AIS-Position…", + "awaiting usable AIS positions…": "warten auf nutzbare AIS-Positionen…", + "awaiting first AIS message…": "warten auf erste AIS-Nachricht…", + "Resume": "Fortsetzen", + "Pause": "Pause", + "Error: {message}": "Fehler: {message}", + "Needs {envVars} — add it in Provider Settings": "Benötigt {envVars}: in den Anbieter-Einstellungen hinzufügen", + "{label} is unavailable": "{label} ist nicht verfügbar", + "This map stack": "Dieser Kartenstapel", + "{label} unavailable: {hint}": "{label} nicht verfügbar: {hint}", + "Visible map targets": "Sichtbare Kartenziele", + "Ready": "Bereit", + "Loaded: {scene} / {shot}": "Geladen: {scene} / {shot}", + "Cannot capture shot: camera not ready": "Aufnahme nicht möglich: Kamera nicht bereit", + "Captured: {scene} / {shot}": "Aufgenommen: {scene} / {shot}", + "Select a shot first": "Zuerst eine Aufnahme wählen", + "Updated: {scene} / {shot}": "Aktualisiert: {scene} / {shot}", + "Camera unavailable — exit cockpit first": "Kamera nicht verfügbar: zuerst Cockpit verlassen", + "No shots to run": "Keine Aufnahmen zum Abspielen", + "Project exported": "Projekt exportiert", + "Could not exit {mode} — scene layers may be refused": "{mode} konnte nicht beendet werden: Szenenebenen werden eventuell abgelehnt", + "Running {index}/{total}: {scene} / {shot}": "Läuft {index}/{total}: {scene} / {shot}", + "Scene run complete": "Szenenlauf abgeschlossen", + "Global Flights Radar": "Globales Flugradar", + "Orbital Watch": "Orbitalwache", + "Thermal Threat Board": "Thermische Bedrohungslage", + "City Overload": "Stadt-Überlast", + "Omniscience Pullback": "Rückflug zur Weltübersicht", + "Shot {n}": "Aufnahme {n}", + "Density": "Dichte", + "Bloom": "Bloom", + "VISUAL PRESETS": "VISUELLE PRESETS", + "LOCATION": "STANDORT", + "CONTACTS — nearest planes · vessels · sites": "KONTAKTE: nächstgelegene Flugzeuge · Schiffe · Standorte", + "SPACE MISSIONS — launches & orbital assets": "RAUMMISSIONEN: Starts & Orbit-Objekte", + "LIVE SIGNALS": "LIVE-SIGNALE", + "Contacts could not complete the requested transition; try again": "Kontakte konnten den gewünschten Wechsel nicht abschließen; erneut versuchen", + "CCTV layer unavailable": "CCTV-Ebene nicht verfügbar", + "Link copied!": "Link kopiert!", + "Copy failed": "Kopieren fehlgeschlagen", + "CCTV calibration saved": "CCTV-Kalibrierung gespeichert", + "CCTV calibration reset": "CCTV-Kalibrierung zurückgesetzt", + "ADJUST ON": "ANPASSEN EIN", + "ADJUST": "ANPASSEN", + "UNKNOWN": "UNBEKANNT", + "ERROR": "FEHLER", + "SOURCE · UNKNOWN": "QUELLE · UNBEKANNT", + "FRAME · LOADING": "FRAME · LÄDT", + "FRAME · UNAVAILABLE": "FRAME · NICHT VERFÜGBAR", + "CALIBRATED": "KALIBRIERT", + "CURATED": "KURATIERT", + "RAW PRIOR": "ROH-SCHÄTZUNG", + "CCTV ON": "CCTV EIN", + "CCTV OFF": "CCTV AUS", + "VIEWSHED ON": "SICHTBEREICH EIN", + "COVERAGE ON": "ABDECKUNG EIN", + "COVERAGE OFF": "ABDECKUNG AUS", + "AUTO HOP ON": "AUTO-WECHSEL EIN", + "AUTO HOP OFF": "AUTO-WECHSEL AUS", + "PROJECTION ON": "PROJEKTION EIN", + "PROJECTION OFF": "PROJEKTION AUS", + "CAL · EDITED (UNSAVED)": "KAL. · GEÄNDERT (UNGESPEICHERT)", + "CAL · {badge}": "KAL. · {badge}", + "MONITOR": "MONITOR", + "OFF": "AUS", + "{city} · HDG {heading} · FOV {fov} · RANGE {range}m · {projection}{calBadge} · {provider}{status}": "{city} · HDG {heading} · FOV {fov} · REICHW. {range} m · {projection}{calBadge} · {provider}{status}", + "{count} cameras loaded · click a camera to activate": { + "one": "{count} Kamera geladen · Kamera anklicken zum Aktivieren", + "other": "{count} Kameras geladen · Kamera anklicken zum Aktivieren" + }, + "{count} cameras loaded · enable CCTV to activate": { + "one": "{count} Kamera geladen · CCTV aktivieren", + "other": "{count} Kameras geladen · CCTV aktivieren" + }, + "{count} more cameras outside this view · pan or zoom out": { + "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.", + "No summary available.": "Keine Zusammenfassung verfügbar.", + "loading frames": "Frames werden geladen", + "camera grid ready": "Kameraraster bereit", + "Clearing selected data layers": "Ausgewählte Datenebenen werden ausgeschaltet", + "Clear selected data layers": "Ausgewählte Datenebenen ausschalten", + "CONDITIONS UNKNOWN": "BEDINGUNGEN UNBEKANNT", + "CLEAR": "KLAR", + "PARTLY CLOUDY": "TEILS BEWÖLKT", + "OVERCAST": "BEDECKT", + "FOG": "NEBEL", + "DRIZZLE": "NIESELREGEN", + "RAIN": "REGEN", + "RAIN SHOWERS": "REGENSCHAUER", + "SNOW SHOWERS": "SCHNEESCHAUER", + "THUNDERSTORM": "GEWITTER", + "MIXED CONDITIONS": "WECHSELHAFT", + "CYCLE ON": "WECHSEL EIN", + "CYCLE OFF": "WECHSEL AUS", + "Stop automatic page cycling. Previous, Next, and the SIG/NEWS/LOCAL tabs stay available.": "Automatischen Seitenwechsel stoppen. Zurück, Weiter und die Tabs SIG/NEWS/LOKAL bleiben verfügbar.", + "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.": "Briefing-Seiten automatisch alle 9 Sekunden wechseln (Signale → News → Lokal). Pausiert, solange sich der Mauszeiger über dem Bereich befindet oder dieser den Fokus hat. Live-Signaldaten werden in beiden Fällen laufend aktualisiert.", + "ACQUIRING REGIONAL NEWS": "REGIONALE NEWS WERDEN ERFASST", + "REGIONAL NEWS UNAVAILABLE": "REGIONALE NEWS NICHT VERFÜGBAR", + "RESOLVING REGION": "REGION WIRD ERMITTELT", + "REGION UNAVAILABLE": "REGION NICHT VERFÜGBAR", + "NO RECENT LOCATION MATCHES": "KEINE AKTUELLEN ORTSTREFFER", + "{domain} · {age}": "{domain} · {age}", + "CLOUD {pct}%": "WOLKEN {pct} %", + "CLOUD UNKNOWN": "WOLKEN UNBEKANNT", + "{source} · LOCATION QUERY": "{source} · ORTSABFRAGE", + "REGIONAL NEWS": "REGIONALE NEWS", + "CONTEXT STANDBY": "KONTEXT STANDBY", + "ENABLE GLOBAL CONTEXT FOR PROXIMITY PINGS": "GLOBALEN KONTEXT FÜR NÄHE-PINGS AKTIVIEREN", + "CONTACT LOST · LAST KNOWN READOUT · NOT AN ALL-CLEAR": "KONTAKT VERLOREN · LETZTER STAND · KEINE ENTWARNUNG", + "CONTACT LOST · {subject}": "KONTAKT VERLOREN · {subject}", + "SUBJECT LEFT ITS FEED · READOUT HOLDING LAST KNOWN": "OBJEKT NICHT MEHR IM FEED · ANZEIGE HÄLT LETZTEN STAND", + "{cohort} · {contact}": "{cohort} · {contact}", + "NO AVAILABLE EXAMPLE": "KEIN BEISPIEL VERFÜGBAR", + "{cohort}, Unavailable": "{cohort}, nicht verfügbar", + "AHEAD": "VORAUS", + "{side} {angle}": "{side} {angle}", + "L": "L", + "R": "R", + "{count} INPUTS UNKNOWN · NOT AN ALL-CLEAR": { + "one": "STATUS UNKLAR: {count} QUELLE · KEINE ENTWARNUNG", + "other": "STATUS UNKLAR: {count} QUELLEN · KEINE ENTWARNUNG" + }, + "AVAILABLE INPUTS CURRENT · NOT AN ALL-CLEAR": "VERFÜGBARE QUELLEN AKTUELL · KEINE ENTWARNUNG", + "AIRCRAFT": "FLUGZEUG", + "ACQUIRING SURFACE": "OBERFLÄCHE WIRD ERFASST", + "SURFACE FALLBACK": "OBERFLÄCHEN-FALLBACK", + "STALE FEED": "VERALTETER FEED", + "LIVE TRACK": "LIVE-TRACK", + "COMMERCIAL": "ZIVIL", + "{aircraftClass} · {feedState} · COURSE ALIGNED": "{aircraftClass} · {feedState} · IN KURSRICHTUNG", + "ARROW · ESTIMATED DIRECTION": "PFEIL · GESCHÄTZTE RICHTUNG", + "ROUTE DATA UNAVAILABLE": "ROUTENDATEN NICHT VERFÜGBAR", + "DEST {bearing}": "DEST {bearing}", + "Disable cockpit weather effects": "Cockpit-Wettereffekte deaktivieren", + "Enable cockpit weather effects": "Cockpit-Wettereffekte aktivieren", + "ON": "EIN", + "Normal": "Normal", + "Night vision": "Nachtsicht", + "Thermal": "Wärmebild", + "Anime": "Anime", + "Noir": "Noir", + "Current cockpit vision style: {style}. Activate for next style.": "Aktueller Cockpit-Sichtstil: {style}. Aktivieren für den nächsten Stil.", + "Current style: {style} — click for next": "Aktueller Stil: {style}. Klicken für den nächsten", + "Collapse Contact panel": "Kontaktbereich einklappen", + "Expand Contact panel": "Kontaktbereich ausklappen", + "Collapse contact panel": "Kontaktbereich einklappen", + "Expand contact panel": "Kontaktbereich ausklappen", + "Collapse cockpit briefing panel": "Cockpit-Briefing-Bereich einklappen", + "Expand cockpit briefing panel": "Cockpit-Briefing-Bereich ausklappen", + "Collapse briefing panel": "Briefing-Bereich einklappen", + "Expand briefing panel": "Briefing-Bereich ausklappen", + "OBSERVED / MAPPED PINGS": "BEOBACHTETE / KARTIERTE PINGS", + "SOURCE-BACKED EVENTS · NO SYNTHETIC NEWS": "QUELLENBASIERTE EREIGNISSE · KEINE SYNTHETISCHEN NEWS", + "LATEST LOCATION-MATCHED REPORTING": "AKTUELLE ORTSBEZOGENE BERICHTERSTATTUNG", + "LOCAL INFO": "LOKALE INFOS", + "PLACE / CONDITIONS / POSITION": "ORT / BEDINGUNGEN / POSITION", + "TIME UNKNOWN": "ZEIT UNBEKANNT", + "{count}M AGO": "VOR {count} MIN", + "{count}H AGO": "VOR {count} H", + "{count}D AGO": "VOR {count} T", + "DIR UNKNOWN": "RICHTUNG UNBEKANNT", + "Select flight {title}": "Flug {title} auswählen", + "{aircraftClass} · CURRENT": "{aircraftClass} · AKTUELL", + "MILITARY FLIGHT": "MILITÄRFLUG", + "COMMERCIAL FLIGHT": "ZIVILFLUG", + "{aircraftClass} · {distance}": "{aircraftClass} · {distance}", + "DISTANCE UNKNOWN": "ENTFERNUNG UNBEKANNT", + "{count} INPUTS UNKNOWN": { + "one": "STATUS UNKLAR: {count} QUELLE", + "other": "STATUS UNKLAR: {count} QUELLEN" + }, + "SOURCE STATUS UNAVAILABLE": "QUELLENSTATUS NICHT VERFÜGBAR", + "Restore real aircraft": "Echtes Flugzeug wiederherstellen", + "Reclassify as TR-3B": "Als TR-3B umklassifizieren", + "TRACK ACQUIRED": "TRACK ERFASST", + "{label} · COURSE {heading}°": "{label} · KURS {heading}°", + "Context could not restore every layer; try again": "Kontext konnte nicht alle Ebenen wiederherstellen; erneut versuchen", + "No selected data layers": "Keine Datenebenen ausgewählt", + "{count} data layers could not be cleared": { + "one": "{count} Datenebene konnte nicht ausgeschaltet werden", + "other": "{count} Datenebenen konnten nicht ausgeschaltet werden" + }, + "Cleared {count} data layers": { + "one": "{count} Datenebene ausgeschaltet", + "other": "{count} Datenebenen ausgeschaltet" + }, + "Selected data layers could not be cleared": "Ausgewählte Datenebenen konnten nicht ausgeschaltet werden", + "Space Missions could not complete the requested transition; try again": "Raummissionen konnten den gewünschten Wechsel nicht abschließen; erneut versuchen", + "Nearby installations refreshed": "Anlagen in der Nähe aktualisiert", + "Nearby installations could not be refreshed; try again": "Anlagen in der Nähe konnten nicht aktualisiert werden; erneut versuchen", + "Space Missions cancellation could not restore the previous layer state": "Abbruch der Raummissionen konnte den vorherigen Ebenenstatus nicht wiederherstellen", + "That layer is unavailable in the current Context mode": "Diese Ebene ist im aktuellen Kontextmodus nicht verfügbar", + "{layerId} could not {action} cleanly": "{layerId} konnte nicht sauber {action}", + "start": "starten", + "stop": "beendet werden", + "{mode} could not start because another layer did not stop cleanly": "{mode}: Start nicht möglich, da eine andere Ebene nicht sauber beendet wurde", + "Space Missions": "Raummissionen", + "Context": "Kontext", + "Live Flights": "Live-Flüge", + "Military Flights": "Militärflüge", + "Earthquakes (24h)": "Erdbeben (24 h)", + "Satellites": "Satelliten", + "Space Missions (30d)": "Raummissionen (30 T)", + "Street Traffic": "Straßenverkehr", + "CCTV": "CCTV", + "Radio": "Radio", + "Bikeshare": "Bikesharing", + "Live AIS Vessels": "Live-AIS-Schiffe", + "Mapped Installations": "Kartierte Anlagen", + "Global Context": "Globaler Kontext", + "Datacenters": "Rechenzentren", + "Dams": "Staudämme", + "Submarine Cables": "Seekabel", + "FIRMS Active Fires": "Aktive Brände (FIRMS)", + "ENABLING": "WIRD AKTIVIERT", + "DISABLING": "WIRD DEAKTIVIERT", + "{state} · {source}": "{state} · {source}", + "UNCERTAIN · {source} · lifecycle state requires reconciliation": "UNKLAR · {source} · Lebenszyklus-Status muss abgeglichen werden", + "{state} · {source} · {detail} · retry {seconds}s": "{state} · {source} · {detail} · neuer Versuch in {seconds} s", + "{state} · {source} · {detail}": "{state} · {source} · {detail}", + "never": "nie", + "{source} · {loadingLabel}": "{source} · {loadingLabel}", + "{accepted} of {raw} records accepted": "{accepted} von {raw} Datensätzen übernommen", + "incomplete snapshot": "unvollständiger Snapshot", + "{state} · {source} · {detail} · {ago}": "{state} · {source} · {detail} · {ago}", + "{state} · {source} · {ago} · retrying in {seconds}s": "{state} · {source} · {ago} · neuer Versuch in {seconds} s", + "{state} · {source} · {ago}": "{state} · {source} · {ago}", + "{source} · {ago}": "{source} · {ago}", + "UNCERTAIN": "UNKLAR", + "{name}: {state}": "{name}: {state}", + "{count}s ago": "vor {count} s", + "CONNECTING": "VERBINDET", + "📍 Location: --": "📍 Standort: --", + "Landmark: --": "Wahrzeichen: --", + "Searched location": "Gesuchter Ort", + "ORBIT": "ORBIT", + "Location not found": "Ort nicht gefunden", + "Search failed": "Suche fehlgeschlagen", + "Fly to a POI first": "Zuerst zu einem POI fliegen", + "Reset to full globe view": "Auf Vollglobus-Ansicht zurücksetzen", + "Reset cockpit to full globe view": "Cockpit auf Vollglobus-Ansicht zurücksetzen", + "Resetting to full globe view": "Zurücksetzen auf Vollglobus-Ansicht", + "Resetting cockpit to full globe view": "Cockpit wird auf Vollglobus-Ansicht zurückgesetzt", + "MAP": "KARTE", + "panel": "Bereich", + "Expand {name}": "{name} ausklappen", + "Collapse {name}": "{name} einklappen", + "Expand Radio": "Radio ausklappen", + "Collapse Radio": "Radio einklappen", + "Expand Radio section": "Radio-Bereich ausklappen", + "Collapse Radio section": "Radio-Bereich einklappen", + "Panel layout updated — positions reset to new defaults": "Bereichslayout aktualisiert – Positionen auf neue Standardwerte zurückgesetzt", + "Collapse Cockpit display options": "Cockpit-Anzeigeoptionen einklappen", + "Expand Cockpit display options": "Cockpit-Anzeigeoptionen ausklappen", + "Collapse Cockpit Radio controls": "Cockpit-Radio-Steuerung einklappen", + "Expand Cockpit Radio controls": "Cockpit-Radio-Steuerung ausklappen", + "NO STATIONS": "KEINE SENDER", + "NO STATION AVAILABLE": "KEIN SENDER VERFÜGBAR", + "{name}, station {index} of {total}": "{name}, Sender {index} von {total}", + "No station available": "Kein Sender verfügbar", + "OFF AIR": "NICHT AUF SENDUNG", + "STATION UNAVAILABLE": "SENDER NICHT VERFÜGBAR", + "Radio could not {action} cleanly": "Radio konnte nicht sauber {action}", + "SYNC": "SYNC", + "RECONCILE": "ABGLEICHEN", + "DISABLE": "AUS", + "ENABLE": "EIN", + "Reconcile Radio — lifecycle uncertain": "Radio abgleichen: Lebenszyklus unklar", + "Disable Radio": "Radio deaktivieren", + "Enable Radio": "Radio aktivieren", + "DIRECTORY BAND": "SENDERSKALA", + "{category} BAND": "SENDERSKALA · {category}", + "NO STATION SELECTED": "KEIN SENDER GEWÄHLT", + "Directory metadata only": "Nur Verzeichnis-Metadaten", + "Loading station directory…": "Senderverzeichnis wird geladen…", + "Choose a globe marker or use next.": "Globus-Marker wählen oder Weiter nutzen.", + "TAGS · {tags}": "TAGS · {tags}", + "Play": "Abspielen", + "PLAY": "PLAY", + "selected": "gewählter", + "nearest": "nächstgelegener", + "{action} {target} radio station": "{action}: {target} Radiosender", + "SYNCING": "SYNC", + "READY": "BEREIT", + "station": "Sender", + " · stale/degraded directory": " · veraltetes/eingeschränktes Verzeichnis", + " · degraded directory": " · eingeschränktes Verzeichnis", + " · stale directory": " · veraltetes Verzeichnis", + " · outside current filter": " · außerhalb des aktuellen Filters", + "Ready — playback starts only from your action": "Bereit: Wiedergabe startet nur manuell", + "Radio off": "Radio aus", + "Connecting directly to broadcaster…": "Direktverbindung zum Sender wird aufgebaut…", + "Buffering broadcaster stream…": "Sender-Stream wird gepuffert…", + "Playing {station}": "{station} läuft", + "Paused {station}": "{station} pausiert", + "Broadcaster stream unavailable": "Sender-Stream nicht verfügbar", + " · muted during voice interaction": " · stumm bei Sprachinteraktion", + " · restoring volume after voice": " · Lautstärke wird nach der Sprachinteraktion wiederhergestellt", + " · static indicates no broadcaster audio": " · Rauschen bedeutet: kein Sender-Audio", + " · tuning static until broadcaster starts": " · Rauschen, bis der Sender startet", + "Station unavailable after directory refresh — choose another channel": "Sender nach Verzeichnisaktualisierung nicht verfügbar: anderen Kanal wählen", + "Radio is enabling…": "Radio wird aktiviert…", + "Radio is disabling…": "Radio wird deaktiviert…", + "Radio lifecycle is uncertain — use Enable or Disable to reconcile": "Radio-Lebenszyklus unklar: zum Abgleichen EIN oder AUS wählen", + "Scene action failed": "Szenenaktion fehlgeschlagen", + "Delete shot \"{shot}\"?": "Aufnahme „{shot}“ löschen?", + "SPARSE": "DÜNN", + "BALANCED": "AUSGEWOGEN", + "DEL": "ENTF.", + "entity": "Objekt", + "Shared {subject} follow expired": "Geteilte Verfolgung abgelaufen: {subject}", + "Shared {subject} could not be restored — feed unavailable": "Geteilt: {subject} konnte nicht wiederhergestellt werden – Feed nicht verfügbar", + "Shared {subject} is unavailable": "Geteilt: {subject} ist nicht verfügbar", + "ACQUIRING": "ERFASSUNG", + "SHARED {subject}": "GETEILT: {subject}", + "LOADING LIVE DATA": "LIVE-DATEN WERDEN GELADEN", + "LOAD COMPLETE": "LADEN ABGESCHLOSSEN", + "LOAD CANCELLED": "LADEN ABGEBROCHEN", + "LOAD FAILED": "LADEN FEHLGESCHLAGEN", + "LIVE DATA OFF": "LIVE-DATEN AUS", + "MAPPED SITES LOADED": "KARTIERTE STANDORTE GELADEN", + "RETRYING MAPPED SITES": "KARTIERTE STANDORTE: NEUER VERSUCH", + "FETCHING MAPPED SITES": "KARTIERTE STANDORTE WERDEN ABGERUFEN", + "TURNING OFF LIVE DATA": "LIVE-DATEN WERDEN AUSGESCHALTET", + "REFRESHING LIVE DATA": "LIVE-DATEN WERDEN AKTUALISIERT", + "syncing road network": "Straßennetz wird synchronisiert", + "retry pending": "neuer Versuch steht aus", + "retrying in {seconds}s": "neuer Versuch in {seconds} s", + "Celestial ring — reveal the full globe": "Himmelsring: ganzen Globus zeigen", + "Celestial ring — available in Normal style": "Himmelsring: im Stil Normal verfügbar", + "Detection overlay: {mode}": "Erkennungs-Overlay: {mode}", + "Detection overlay: off": "Erkennungs-Overlay: aus", + "DETECT": "ERKENN.", + "voice|LISTENING": "HÖRT ZU", + "voice|EXECUTING": "FÜHRT AUS", + "AI AGENT": "KI-AGENT", + "Voice model tier — applies next session": "Sprachmodell-Stufe: gilt ab nächster Sitzung", + "Estimated session cost": "Geschätzte Sitzungskosten", + "Voice control — activate to toggle voice; hold Space to speak": "Sprachsteuerung: aktivieren zum Umschalten; Leertaste halten zum Sprechen", + "VOICE STANDBY": "SPRACHE STANDBY", + "VOICE CONTROL": "SPRACHSTEUERUNG", + "Hold Space to speak · tap Space to activate focused controls": "Leertaste halten zum Sprechen · kurz drücken, um fokussierte Elemente zu aktivieren", + "VOICE SYSTEM ERROR": "SPRACHSYSTEM-FEHLER", + "DISMISS": "SCHLIESSEN", + "Check microphone permission and network access, then try again.": "Mikrofonberechtigung und Netzwerkzugriff prüfen, dann erneut versuchen.", + "Release Space to send": "Leertaste loslassen zum Senden", + "Hold Space to talk": "Leertaste halten zum Sprechen", + "Requesting microphone": "Mikrofonzugriff wird angefordert", + "Ask or command": "Fragen oder Befehl geben", + "Running command": "Befehl läuft", + "Voice off": "Sprache aus", + "WebRTC microphone support unavailable": "WebRTC-Mikrofonunterstützung nicht verfügbar", + "VOICE UNAVAILABLE": "SPRACHE N. V.", + "VOICE ACTIVE": "SPRACHE AKTIV", + "Voice session could not be started.": "Sprachsitzung konnte nicht gestartet werden.", + "Next session: {pendingId} — this session stays on {modelId}": "Nächste Sitzung: {pendingId}. Diese Sitzung bleibt bei {modelId}", + "Voice model: {pendingId} — click to switch to {target}; applies next session": "Sprachmodell: {pendingId}. Klicken zum Wechsel auf {target}; gilt ab nächster Sitzung", + "Estimated session cost on {modelId} — {responses} response(s). Warns at {warn}, ends the session at {cap}.": "Geschätzte Sitzungskosten mit {modelId}. {responses} Antwort(en). Warnung bei {warn}, Sitzungsende bei {cap}.", + "{tier} applies next session": "{tier} gilt ab nächster Sitzung", + "Session ended — cost cap {cost}": "Sitzung beendet: Kostenlimit {cost}", + "Radio did not start": "Radio wurde nicht gestartet", + "Aircraft cockpit view": "Flugzeug-Cockpitansicht", + "LEVEL": "HORIZONT", + "Estimated destination direction": "Geschätzte Richtung zum Ziel", + "DEST ---°": "DEST ---°", + "OPTICAL PLANE · 01": "OPTISCHE EBENE · 01", + "VISOR LOCK · ACTIVE": "VISIER-LOCK · AKTIV", + "GROUND SPEED · KTS": "GESCHW. ÜBER GRUND · KN", + "ALTITUDE · FT": "HÖHE · FT", + "FIRST PERSON": "EGO-PERSPEKTIVE", + "LIVE TRACK · COURSE ALIGNED": "LIVE-TRACK · IN KURSRICHTUNG", + "Cockpit vision style": "Sichtstil im Cockpit", + "Previous cockpit vision style": "Vorheriger Cockpit-Sichtstil", + "Previous vision style": "Vorheriger Sichtstil", + "Current cockpit vision style: NORMAL. Activate for next style.": "Aktueller Cockpit-Sichtstil: NORMAL. Aktivieren für den nächsten Stil.", + "Current style: NORMAL — click for next": "Aktueller Stil: NORMAL. Klicken für den nächsten", + "CURRENT": "AKTUELL", + "Next cockpit vision style": "Nächster Cockpit-Sichtstil", + "Next vision style": "Nächster Sichtstil", + "Current aircraft heading": "Aktueller Flugzeugkurs", + "GROUND SPEED": "GESCHW. ÜBER GRUND", + "ALTITUDE": "HÖHE", + "Contact cockpit summary": "Cockpit-Zusammenfassung des Kontakts", + "CONTACT": "KONTAKT", + "CONTACTS · 250 KM": "KONTAKTE · 250 KM", + "Contact navigation": "Kontaktnavigation", + "CONTEXT ONLY": "NUR KONTEXT", + "Nearby cohort counts": "Anzahl Kontakte in der Nähe", + "NEAREST OBSERVED / MAPPED": "NÄCHSTGELEGENES OBJEKT · BEOBACHTET / KARTIERT", + "AVAILABLE INPUTS ONLY · NOT AN ALL-CLEAR": "NUR VERFÜGBARE QUELLEN · KEINE ENTWARNUNG", + "Cockpit briefing carousel": "Cockpit-Briefing-Karussell", + "Estimated flight plan": "Geschätzter Flugplan", + "ESTIMATED FLIGHT PLAN": "GESCHÄTZTER FLUGPLAN", + "FROM": "VON", + "TO": "NACH", + "Cockpit briefing controls": "Cockpit-Briefing-Steuerung", + "Previous briefing page": "Vorherige Briefing-Seite", + "Next briefing page": "Nächste Briefing-Seite", + "Live signals": "Live-Signale", + "Latest regional news": "Aktuelle regionale Nachrichten", + "Location-based information": "Ortsbezogene Informationen", + "TEMP": "TEMP", + "WIND": "WIND", + "SKY": "HIMMEL", + "PRECIP": "NIEDERSCHL.", + "Show Live Signals": "Live-Signale anzeigen", + "NEWS": "NEWS", + "Show Regional News": "Regionale News anzeigen", + "Show Local Info": "Lokale Infos anzeigen", + "Cockpit display and Radio controls": "Cockpit-Anzeige und Radio-Steuerung", + "DISPLAY": "ANZEIGE", + "Cockpit display options": "Cockpit-Anzeigeoptionen", + "RADIO": "RADIO", + "Cockpit compact Radio controls": "Kompakte Cockpit-Radio-Steuerung", + "Previous filtered radio station": "Vorheriger gefilterter Radiosender", + "Play selected radio station": "Gewählten Radiosender abspielen", + "Next filtered radio station": "Nächster gefilterter Radiosender", + "VOLUME": "LAUTSTÄRKE", + "Cockpit Radio volume": "Cockpit-Radio-Lautstärke", + "View switcher": "Ansichtsumschalter", + "Exit cockpit and return to full globe view": "Cockpit verlassen und zur Vollglobus-Ansicht zurückkehren", + "RESET": "RESET", + "Exit cockpit view": "Cockpitansicht verlassen", + "EXIT COCKPIT": "COCKPIT VERLASSEN", + "Navigation, voice, and visual preset controls": "Steuerung für Navigation, Sprache und visuelle Presets", + "Expand Visual Presets": "Visuelle Presets ausklappen", + "Pin visual presets": "Visuelle Presets anheften", + "Keep visual presets open": "Visuelle Presets geöffnet lassen", + "Show the globe without a visual filter.": "Globus ohne visuellen Filter anzeigen.", + "Emulate a green phosphor CRT with scanlines and screen curvature.": "Grüne Phosphor-Röhre mit Scanlines und Bildschirmkrümmung nachahmen.", + "Simulate night-vision goggles with green intensification and a tube vignette.": "Nachtsichtgerät mit grüner Restlichtverstärkung und Röhrenvignette simulieren.", + "Simulate FLIR-style thermal contrast. Turn up Ironbow for color.": "FLIR-artigen Wärmekontrast simulieren. Ironbow aufdrehen für Farbe.", + "Apply bright cel-shaded color and illustrated outlines.": "Leuchtende Cel-Shading-Farben und gezeichnete Konturen anwenden.", + "Apply high-contrast monochrome film-noir grading.": "Kontrastreiche monochrome Film-noir-Farbgebung anwenden.", + "MAP SOURCE": "KARTENQUELLE", + "Map source": "Kartenquelle", + "Style": "Stil", + "Expand LOCATION": "STANDORT ausklappen", + "Collapse panel": "Bereich einklappen", + "Pin location tray": "Standortleiste anheften", + "Keep location tray open": "Standortleiste geöffnet lassen", + "Search any location": "Beliebigen Ort suchen", + "Search location by name or coordinates": "Ort nach Namen oder Koordinaten suchen", + "Search any location...": "Ort suchen...", + "WEATHER": "WETTER", + "CONTEXT": "KONTEXT", + "Expand panel": "Bereich ausklappen", + "Context mode": "Kontextmodus", + "CONTACTS": "KONTAKTE", + "Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "Wechselt zwischen den nächstgelegenen Kontakten des gewählten Typs: Flugzeuge, Schiffe, Anlagen. Satelliten werden unabhängig verfolgt.", + "SPACE MISSIONS": "RAUMMISSIONEN", + "SELECT CONTEXT": "KONTEXT WÄHLEN", + "Contact Context actions": "Aktionen für Kontaktkontext", + "COCKPIT": "COCKPIT", + "SEARCH NEARBY SITES": "STANDORTE IN DER NÄHE SUCHEN", + "Reclassify tracked contact as TR-3B": "Verfolgten Kontakt als TR-3B umklassifizieren", + "CONTACTS CONTEXT OFF": "KONTAKTKONTEXT AUS", + "SELECT CONTACTS TO LOAD OBSERVED / MAPPED PROXIMITY": "KONTAKTE WÄHLEN FÜR BEOBACHTETE / KARTIERTE OBJEKTE IM UMKREIS", + "Available Space Missions": "Verfügbare Raummissionen", + "AVAILABLE MISSIONS": "VERFÜGBARE MISSIONEN", + "SELECT A MISSION TO INSPECT": "MISSION ZUM ANSEHEN WÄHLEN", + "LOADING 30-DAY MISSION INDEX": "30-TAGE-MISSIONSINDEX WIRD GELADEN", + "TAB PREVIEWS · ENTER / SPACE SELECTS": "TAB: VORSCHAU · ENTER / LEERTASTE: AUSWAHL", + "Internet radio and local SDR": "Internetradio und lokales SDR", + "STATION TAG": "KATEGORIE", + "Filter stations by station tag": "Sender nach Kategorie filtern", + "All": "Alle", + "Enable Radio, then choose a globe marker or use next.": "Radio aktivieren, dann einen Globus-Marker wählen oder Weiter nutzen.", + "DRAG TO TUNE": "ZUM ABSTIMMEN ZIEHEN", + "ALL · DRAG THE NEEDLE": "ALLE · NADEL ZIEHEN", + "SNAPS TO AVAILABLE STATIONS": "RASTET BEI VERFÜGBAREN SENDERN EIN", + "Radio playback": "Radiowiedergabe", + "Previous filtered station": "Vorheriger gefilterter Sender", + "Play selected station": "Gewählten Sender abspielen", + "Next filtered station": "Nächster gefilterter Sender", + "STOP": "STOPP", + "Stop radio playback": "Radiowiedergabe stoppen", + "Radio volume": "Radio-Lautstärke", + "STATION SITE": "SENDER-WEBSITE", + "Audio connects directly to the broadcaster after you press play. Your IP is visible to that broadcaster.": "Beim Starten der Wiedergabe wird eine direkte Verbindung zum Sender hergestellt. Die IP-Adresse ist für diesen Sender sichtbar.", + "Intelligence HUD (H)": "Lagebild-HUD (H)", + "Layout": "Layout", + "HUD layout": "HUD-Layout", + "Tactical": "Taktisch", + "Minimal": "Minimal", + "Detection Overlay (D)": "Erkennungs-Overlay (D)", + "Detection overlay": "Erkennungs-Overlay", + "Detection label density": "Dichte der Erkennungsbeschriftungen", + "Allocation": "Zuteilung", + "Detection label allocation": "Zuteilung der Erkennungsbeschriftungen", + "Elastic": "Elastisch", + "Weighted": "Gewichtet", + "Fade": "Ausblenden", + "Detection fade distance": "Ausblendeabstand der Erkennung", + "World-overlay fade distance outside the keyhole as a percentage of its radius": "Ausblendeabstand des Welt-Overlays außerhalb des Schlüssellochs, in Prozent seines Radius", + "Outside": "Außen", + "Detection opacity outside the keyhole": "Erkennungsdeckkraft außerhalb des Schlüssellochs", + "World-overlay label and card opacity beyond the fade distance": "Deckkraft von Beschriftungen und Infokarten im Welt-Overlay jenseits des Ausblendeabstands", + "PARAMETERS": "PARAMETER", + "3D aircraft — flat icons zoomed out, 3D models up close": "3D-Flugzeuge: flache Symbole aus der Ferne, 3D-Modelle aus der Nähe", + "Models": "Modelle", + "3D model coverage": "3D-Modellabdeckung", + "Proximity": "Nähe", + "Scope — the circular viewport mask": "Scope: die kreisrunde Ansichtsmaske", + "Scope": "Scope", + "Feather": "Weichzeichnung", + "Scope edge feather": "Weichzeichnung des Scope-Rands", + "Scope edge feather as a percentage of the keyhole radius": "Weichzeichnung des Scope-Rands in Prozent des Schlüsselloch-Radius", + "Celestial": "Himmelsring", + "Hide UI chrome": "Oberfläche ausblenden", + "Clean UI": "Reine Ansicht", + "Bloom / Glow": "Bloom / Leuchten", + "Bloom intensity": "Bloom-Stärke", + "Sharpening": "Schärfen", + "Sharpen": "Schärfen", + "Sharpen intensity": "Schärfestärke", + "EXIT CLEAN VIEW": "REINE ANSICHT BEENDEN", + "Return UI controls": "Bedienelemente wieder einblenden", + "Initializing photorealistic world...": "Fotorealistische Welt wird initialisiert...", + "DATA LAYERS": "DATENEBENEN", + "NEAREST": "NÄCHSTGELEGENE", + "CCTV camera": "CCTV-Kamera", + "CALIBRATION": "KALIBRIERUNG", + "Drag the camera in the world: rings rotate, arrows move, handles set range/FOV": "Kamera in der Welt ziehen: Ringe drehen, Pfeile verschieben, Griffe setzen Reichweite/FOV", + "Camera pose — click a value to type": "Kamerapose: Wert anklicken zum Eingeben", + "Heading (compass °) — click to type": "Ausrichtung (Kompass °): anklicken zum Eingeben", + "Pitch (° up/down) — click to type": "Neigung (° auf/ab): anklicken zum Eingeben", + "Horizontal FOV (°) — click to type": "Horizontales FOV (°): anklicken zum Eingeben", + "Range / monitor-plane distance (m) — click to type": "Reichweite / Abstand der Monitorebene (m): anklicken zum Eingeben", + "Mount height above ground (m) — click to type": "Montagehöhe über Grund (m): anklicken zum Eingeben", + "North offset from catalog position (m) — click to type": "Nordversatz zur Katalogposition (m): anklicken zum Eingeben", + "East offset from catalog position (m) — click to type": "Ostversatz zur Katalogposition (m): anklicken zum Eingeben", + "SAVE CAL": "KAL. SPEICHERN", + "RESET CAL": "KAL. ZURÜCKSETZEN", + "SCENE SUMMARY": "SZENEN-ÜBERSICHT", + "SCENES": "SZENEN", + "Scene recipe": "Szenenrezept", + "NEW": "NEU", + "CAPTURE SHOT": "AUFNAHME SPEICHERN", + "UPDATE SHOT": "AUFNAHME AKTUALISIEREN", + "START": "START", + "EXPORT PRESETS": "PRESETS EXPORTIEREN", + "IMPORT": "IMPORTIEREN", + "RUN LOG": "PROTOKOLL", + "POWER UP": "STARTKLAR", + "GROUND STATION · PROVIDER SETTINGS": "BODENSTATION · ANBIETER-EINSTELLUNGEN", + "Close key setup": "Schlüssel-Einrichtung schließen", + "Power up the globe": "Globus hochfahren", + "The globe already flies keyless. Every key below switches on another real feed — paste one and it's saved into this app's local configuration, then the server restarts itself. Server-side keys stay on this machine; Google Maps and Cesium ion run in the browser and must be provider-restricted. Keys you configured elsewhere are shown but never touched.": "Der Globus fliegt bereits ohne Schlüssel. Jeder Schlüssel unten schaltet einen weiteren echten Feed frei: Einfügen genügt, er wird in der lokalen Konfiguration dieser App gespeichert, danach startet der Server selbst neu. Serverseitige Schlüssel bleiben auf diesem Rechner; die Schlüssel für Google Maps und Cesium ion werden im Browser verwendet und müssen beim jeweiligen Anbieter eingeschränkt werden. Anderswo konfigurierte Schlüssel werden angezeigt, aber nie verändert.", + "SAVE KEYS": "SCHLÜSSEL SPEICHERN", + "ESC to close": "ESC zum Schließen", + "The Google Maps key buys the photorealistic planet — everything else stacks on top.": "Der Google-Maps-Schlüssel liefert den fotorealistischen Planeten, alles andere kommt obendrauf.", + "NO PLACE LEFT BEHIND": "KEIN ORT BLEIBT ZURÜCK", + "ACTIVE STYLE": "AKTIVER STIL", + "Globe actions": "Globus-Aktionen", + "Turn off all selected data layers": "Alle ausgewählten Datenebenen ausschalten", + "Copy share link": "Link zum Teilen kopieren", + "Reset camera and return to full globe view": "Kamera zurücksetzen und zur Vollglobus-Ansicht zurückkehren", + "MISSION CONTROL · FIRST LAUNCH": "MISSION CONTROL · ERSTER START", + "Choose your first view": "Erste Ansicht wählen", + "LIVE CONTACTS": "LIVE-KONTAKTE", + "Aircraft, vessels and nearby intelligence": "Flugzeuge, Schiffe und Lagebild der Umgebung", + "Launches, spacecraft and orbital context": "Starts, Raumfahrzeuge und Orbit-Kontext", + "EXPLORE MANUALLY": "MANUELL ERKUNDEN", + "Begin with a clean globe": "Mit einem leeren Globus beginnen", + "Don't show this again": "Nicht mehr anzeigen", + "ESC to dismiss": "ESC zum Schließen", + "Tip: the GEV MIC button in the dock lets you talk to the map.": "Tipp: Mit der Mikrofontaste im Dock lässt sich die Karte per Sprache steuern.", + "Pick a shape, then click the map.": "Form wählen, dann auf die Karte klicken.", + "Enter to place the pin, Esc to cancel.": "Enter setzt den Pin, Esc bricht ab.", + "Click where the pin goes.": "Auf die Stelle für den Pin klicken.", + "Click {count} more points.": { + "one": "Noch {count} Punkt anklicken.", + "other": "Noch {count} Punkte anklicken." + }, + "Those points are in a line — move one off it to enclose an area.": "Diese Punkte liegen auf einer Linie – einen davon verschieben, um eine Fläche einzuschließen.", + "That line has no length — click somewhere further away.": "Diese Linie hat keine Länge – weiter entfernt klicken.", + " · {max}-point limit reached": " · Limit von {max} Punkten erreicht", + "{measure} · double-click or Enter to finish, Backspace undoes, Esc cancels.{full}": "{measure} · Doppelklick oder Enter beendet, Rücktaste macht rückgängig, Esc bricht ab.{full}", + "drawings on the map": "Zeichnungen auf der Karte", + "That shape already has {count} points — finish it or press Backspace.": "Diese Form hat bereits {count} Punkte – abschließen oder Rücktaste drücken.", + "That point is off the globe — click on the world.": "Dieser Punkt liegt außerhalb des Globus – auf die Erde klicken.", + "That shape could not be placed.": "Diese Form konnte nicht platziert werden.", + "Could not place the shape: {error}": "Form konnte nicht platziert werden: {error}", + "Board cleared.": "Zeichenfläche geleert.", + "{owner} is using the pointer — close it first.": "{owner} verwendet gerade den Zeiger – zuerst schließen.", + "{label} · Detailed outline unavailable": "{label} · Detaillierter Umriss nicht verfügbar", + "Space Missions isolates replay data. Exit the mode to enable {layer}.": "Der Modus Raummissionen isoliert Replay-Daten. Modus verlassen, um {layer} zu aktivieren.", + "Data attribution": "Datenquellen", + "Data provided by:": "Daten bereitgestellt von:", + "Light aircraft": "Leichtflugzeug", + "Glider": "Segelflugzeug", + "Turboprop": "Turboprop", + "Airliner": "Verkehrsflugzeug", + "Widebody": "Großraumflugzeug", + "Four-engine jet": "Vierstrahliger Jet", + "Helicopter": "Hubschrauber", + "Fast jet": "Kampfjet", + "Business jet": "Businessjet", + "Drone": "Drohne", + "General emergency": "Allgemeiner Notfall", + "Unlawful interference": "Widerrechtlicher Eingriff", + "Radio failure": "Funkausfall", + "Downed aircraft": "Abgestürztes Luftfahrzeug", + "Minimum fuel": "Minimum Fuel (Treibstoff knapp)", + "Lifeguard (medical priority)": "Lifeguard (medizinische Priorität)", + "Rasuwagadhi witness video": "Augenzeugenvideo Rasuwagadhi", + "Bhote Koshi valley footage": "Aufnahmen aus dem Bhote-Koshi-Tal", + "Flood footage": "Hochwasseraufnahmen", + "Compilation and casualty claims": "Zusammenschnitt und Opferangaben", + "SOURCE GEOCONFIRMED · WORLD-PINNED": "QUELLE GEOVERIFIZIERT · AUF GLOBUS FIXIERT", + "SOURCE CREDITED · LOCATION UNRESOLVED": "QUELLE GENANNT · ORT UNGEKLÄRT", + "REFERENCE ONLY · CLAIMS UNVERIFIED": "NUR REFERENZ · ANGABEN UNBESTÄTIGT", + "SOURCE GEOCONFIRMED": "QUELLE GEOVERIFIZIERT", + "2021 historical reference versus 2026 post-event imagery; changes are cumulative and include the July 2025 Rasuwagadhi flood. Schematic downstream progression, not modeled arrival time; not an official hazard model.": "Historische Referenz von 2021 gegenüber Bildern nach dem Ereignis von 2026; die Veränderungen sind kumulativ und umfassen die Rasuwagadhi-Flut vom Juli 2025. Schematischer Verlauf flussabwärts, keine modellierte Ankunftszeit; kein offizielles Gefahrenmodell.", + "Immediate collapse viewpoint": "Blickpunkt direkt am Erdrutsch", + "Collapse viewpoint": "Blickpunkt Erdrutsch", + "Debris-dammed lake": "Durch Geröll aufgestauter See", + "Dammed lake": "Aufgestauter See", + "Second landslide": "Zweiter Erdrutsch", + "Gyirong border gate": "Grenzübergang Gyirong", + "Border gate": "Grenzübergang", + "Timure evidence cluster": "Beweiscluster Timure", + "Mailung / Upper Trishuli bridge": "Mailung / Brücke am Upper Trishuli", + "Dandagaun viewpoint restaurant": "Aussichtsrestaurant Dandagaun", + "Dandagaun viewpoint": "Aussichtspunkt Dandagaun", + "Bidur - Trishuli bridge": "Bidur – Trishuli-Brücke", + "Bidur bridge": "Brücke Bidur", + "Devighat / Taadi Khola Bridge": "Devighat / Taadi-Khola-Brücke", + "Devighat bridge": "Brücke Devighat", + "Facebook player API did not initialize.": "Facebook-Player-API wurde nicht initialisiert.", + "Facebook player API could not load.": "Facebook-Player-API konnte nicht geladen werden.", + "Facebook player API timed out.": "Zeitüberschreitung bei der Facebook-Player-API.", + "X embed API did not initialize.": "X-Embed-API wurde nicht initialisiert.", + "X embed could not load.": "X-Einbettung konnte nicht geladen werden.", + "X embed timed out.": "Zeitüberschreitung bei der X-Einbettung.", + "TIMEOUT": "ZEITÜBERSCHREITUNG", + "BLOCKED": "BLOCKIERT", + "Facebook embed unavailable. The post may be private, removed, region-restricted, or blocked.": "Facebook-Einbettung nicht verfügbar. Der Beitrag ist möglicherweise privat, entfernt, regional gesperrt oder blockiert.", + "Preloading {provider} media": "{provider}-Medien werden vorgeladen", + "LOADING X POST…": "X-BEITRAG WIRD GELADEN…", + "The X post is unavailable or cannot be embedded.": "Der X-Beitrag ist nicht verfügbar oder kann nicht eingebettet werden.", + "X POST · PROVIDER CONTROLS": "X-BEITRAG · STEUERUNG DES ANBIETERS", + "{reason} OPEN ORIGINAL.": "{reason} ORIGINAL ÖFFNEN.", + "X embed unavailable": "X-Einbettung nicht verfügbar", + "FACEBOOK · PROVIDER CONTROLS": "FACEBOOK · STEUERUNG DES ANBIETERS", + "Facebook embed unavailable": "Facebook-Einbettung nicht verfügbar", + "AUTOPLAY BLOCKED · USE PROVIDER CONTROLS": "AUTOPLAY BLOCKIERT · STEUERUNG DES ANBIETERS NUTZEN", + "Embedded {provider} media for {source}": "Eingebettete {provider}-Medien für {source}", + "witness source": "Augenzeugenquelle", + "Witness source": "Augenzeugenquelle", + "{provider} · EMBEDDED SOURCE": "{provider} · EINGEBETTETE QUELLE", + "OPEN ORIGINAL ↗": "ORIGINAL ÖFFNEN ↗", + "{provider} media: {source}": "{provider}-Medien: {source}", + "{provider} · PROVIDER CONTROLS": "{provider} · STEUERUNG DES ANBIETERS", + "YOUTUBE {state} · OPEN ORIGINAL": "YOUTUBE {state} · ORIGINAL ÖFFNEN", + "INITIATION": "AUSLÖSUNG", + "DAMMING": "AUFSTAUUNG", + "SECONDARY FAILURE": "FOLGEVERSAGEN", + "BORDER ARRIVAL": "ANKUNFT AN DER GRENZE", + "PROPAGATION": "AUSBREITUNG", + "DOWNSTREAM PASSAGE": "DURCHGANG FLUSSABWÄRTS", + "CONSEQUENCE": "FOLGEN", + "DOWNSTREAM CONSEQUENCE": "FOLGEN FLUSSABWÄRTS", + "EVIDENCE": "BELEGE", + "{year} HISTORICAL REFERENCE": "HISTORISCHE REFERENZ {year}", + "{year} POST-EVENT": "NACH DEM EREIGNIS {year}", + "{count} GEOLOCATED SOURCE-MAP PLACEMENTS": { + "one": "{count} GEOLOKALISIERTE PLATZIERUNG AUF DER QUELLENKARTE", + "other": "{count} GEOLOKALISIERTE PLATZIERUNGEN AUF DER QUELLENKARTE" + }, + "NO PREVIEW": "KEINE VORSCHAU", + "YouTube source": "YouTube-Quelle", + "Facebook source": "Facebook-Quelle", + "SOURCE LINK": "QUELLENLINK", + "{publisher} / OPEN": "{publisher} / ÖFFNEN", + "Bhote Koshi flood reconstruction controls": "Steuerung der Bhote-Koshi-Flutrekonstruktion", + "EVENT RECONSTRUCTION · 26 AUG 2026": "REKONSTRUKTION DES EREIGNISSES · 26. AUG. 2026", + "BHOTE KOSHI OUTBURST FLOOD": "AUSBRUCHSFLUT BHOTE KOSHI", + "Close event layer": "Ereignisebene schließen", + "OBSERVED IMAGERY": "REALE BILDDATEN", + "SCHEMATIC CORRIDOR": "SCHEMATISCHER KORRIDOR", + "2021 HISTORICAL REFERENCE / 2026 POST-EVENT": "HISTORISCHE REFERENZ 2021 / NACH DEM EREIGNIS 2026", + "Historical reference and post-event image split": "Bildvergleich: historische Referenz und Bild nach dem Ereignis", + "RECONSTRUCTION CLOCK": "REKONSTRUKTIONSUHR", + "Schematic downstream progression": "Schematischer Verlauf flussabwärts", + "Story beat navigation": "Navigation durch die Story", + "Previous story beat": "Vorheriger Story-Schritt", + "CAUSE": "URSACHE", + "CAPTURE TIME UNVERIFIED": "AUFNAHMEZEIT UNBESTÄTIGT", + "Next story beat": "Nächster Story-Schritt", + "▶ PLAY": "▶ ABSPIELEN", + "Play the next shot and continue through this scene": "Nächste Aufnahme abspielen und die Szene fortsetzen", + "▶ PLAY SCENE": "▶ SZENE ABSPIELEN", + "◉ CINEMATIC": "◉ KINOMODUS", + "↺ FULL STORY": "↺ GANZE STORY", + "↗ OPEN ORIGINAL": "↗ ORIGINAL ÖFFNEN", + "⌖ LOWER GORGE": "⌖ UNTERE SCHLUCHT", + "FIELD REPORTS · {count}": "FELDBERICHTE · {count}", + "GEOLOCATIONS · {credit}": "GEOLOKALISIERUNGEN · {credit}", + "↗ OPEN PUBLIC MAP": "↗ ÖFFENTLICHE KARTE ÖFFNEN", + " Clouds are preserved from the source imagery.": " Die Wolken stammen aus den Quellbildern.", + "Rasuwagadhi witness": "Augenzeuge Rasuwagadhi", + "Open original source for {source}; capture time unverified": "Originalquelle für {source} öffnen; Aufnahmezeit unbestätigt", + "Historical reference and post-event image divider": "Trennlinie zwischen historischer Referenz und Bild nach dem Ereignis", + "A historical reference {before} percent, B post-event {after} percent": "A historische Referenz {before} Prozent, B nach dem Ereignis {after} Prozent", + "■ RELEASE CAMERA": "■ KAMERA FREIGEBEN", + "Seek Nepal scene clock": "Szenenuhr Nepal spulen", + "NEPAL SCENE CLOCK": "SZENENUHR NEPAL", + "… PLAYING SHOT": "… AUFNAHME LÄUFT", + "▶ PLAY SHOT": "▶ AUFNAHME ABSPIELEN", + "Ⅱ PAUSE": "Ⅱ PAUSE", + "↺ REPLAY CINEMATIC": "↺ KINOMODUS WIEDERHOLEN", + "↺ REPLAY": "↺ WIEDERHOLEN", + "… PLAYING SCENE": "… SZENE LÄUFT", + "Continue {scene} from next shot": "{scene} ab der nächsten Aufnahme fortsetzen", + "current scene": "aktuelle Szene", + "Replay the full reconstruction": "Gesamte Rekonstruktion wiederholen", + "SCENE SHOT": "SZENENAUFNAHME", + "TIME UNVERIFIED": "ZEIT UNBESTÄTIGT", + "{count} SOURCES": { + "one": "{count} QUELLE", + "other": "{count} QUELLEN" + }, + "OUTSIDE IMAGERY SWIPE": "NICHT IM BILDVERGLEICH", + "{scene} / AUTHORED SHOT": "{scene} / GESTALTETE AUFNAHME", + "NEPAL FLOOD INCIDENT": "FLUTEREIGNIS NEPAL", + "Play {shot}": "{shot} abspielen", + "current scene shot": "aktuelle Szenenaufnahme", + "Play reconstruction": "Rekonstruktion abspielen", + "Load previous scene shot": "Vorherige Szenenaufnahme laden", + "Load next scene shot": "Nächste Szenenaufnahme laden", + "SCHEMATIC CORRIDOR, NOT MODELED ARRIVAL TIME.": "SCHEMATISCHER KORRIDOR, KEINE MODELLIERTE ANKUNFTSZEIT.", + "Rasuwa · 26 Aug 2026": "Rasuwa · 26. Aug. 2026", + "Event reconstruction": "Rekonstruktion des Ereignisses", + "BHOTE KOSHI FLOOD INCIDENT": "FLUTEREIGNIS BHOTE KOSHI", + "NEPAL · 26 AUG 2026": "NEPAL · 26. AUG. 2026", + "USGS INITIAL REPORT": "USGS-ERSTMELDUNG", + "26 AUG 2026 · 08:37 NPT": "26. AUG. 2026 · 08:37 NPT", + "28.2710° N · 85.5150° E": "28,2710° N · 85,5150° O", + "INITIAL M4.4 · RECLASSIFIED M5.2 LANDSLIDE": "ZUERST M4,4 · NEU EINGESTUFT ALS M5,2-ERDRUTSCH", + "USGS EVENT us7000tbwb · OPEN SOURCE": "USGS-EREIGNIS us7000tbwb · QUELLE ÖFFNEN", + "Open the USGS initial report for the 26 August 2026 Nepal landslide": "USGS-Erstmeldung zum Erdrutsch in Nepal vom 26. August 2026 öffnen", + "Show the Nepal context callout without a border or country fill": "Kontexthinweis zu Nepal ohne Grenze und Landesfläche zeigen", + "Introduce Nepal, draw its border, then reveal the Bhote Koshi incident": "Nepal vorstellen, die Grenze zeichnen und dann das Bhote-Koshi-Ereignis zeigen", + "CITIES": "STÄDTE", + "Keep the regional incident context and introduce nearby city labels": "Regionalen Ereigniskontext beibehalten und Städtenamen in der Nähe einblenden", + "PLACES": "ORTE", + "Introduce the reconstruction scene key places from trigger to downstream": "Die wichtigsten Orte der Rekonstruktion vom Auslöser bis flussabwärts vorstellen", + "PATH": "PFAD", + "Animate the sourced cyan river-centerline route to Trishuli Bazaar": "Die belegte cyanfarbene Flussmittellinie bis Trishuli Bazaar animieren", + "OVERVIEW": "ÜBERSICHT", + "Keep the completed flood path and location labels visible": "Fertigen Flutpfad und Ortsnamen sichtbar lassen", + "RECORD": "MELDUNG", + "Reveal the trigger record while making a slow camera approach": "USGS-Meldung zum auslösenden Ereignis zeigen, während sich die Kamera langsam nähert", + "{speed} kn": "{speed} kn", + "That file is not valid JSON.": "Diese Datei ist kein gültiges JSON.", + "That JSON file is not GeoJSON.": "Diese JSON-Datei ist kein GeoJSON.", + "That FeatureCollection has no features list.": "Diese FeatureCollection hat keine Feature-Liste.", + "Every GeoJSON feature must have type Feature.": "Jedes GeoJSON-Feature muss den Typ Feature haben.", + "That GeoJSON has invalid geometries.": "Dieses GeoJSON enthält ungültige Geometrien.", + "That GeoJSON contains an unsupported geometry.": "Dieses GeoJSON enthält eine nicht unterstützte Geometrie.", + "That GeoJSON has invalid coordinates.": "Dieses GeoJSON enthält ungültige Koordinaten.", + "That file creates more than {limit} map objects (the limit).": "Diese Datei erzeugt mehr als {limit} Kartenobjekte (das Limit).", + "That file has more than {limit} style definitions and image links after cleanup (the limit).": "Diese Datei enthält nach der Bereinigung mehr als {limit} Stildefinitionen und Bildlinks (das Limit).", + "That file has more than {limit} features (the limit).": "Diese Datei enthält mehr als {limit} Features (das Limit).", + "That file has more than {limit} points (the limit) across {features} features.": "Diese Datei enthält mehr als {limit} Punkte (das Limit) in {features} Features.", + "That file has more than {limit} points (the limit).": "Diese Datei enthält mehr als {limit} Punkte (das Limit).", + "That KMZ unpacks to more than the import limit.": "Diese KMZ ist entpackt größer als das Importlimit.", + "That KMZ is damaged and could not be unpacked.": "Diese KMZ ist beschädigt und konnte nicht entpackt werden.", + "That KMZ is not a readable zip archive.": "Diese KMZ ist kein lesbares ZIP-Archiv.", + "That KMZ uses a zip format this import does not read.": "Diese KMZ verwendet ein ZIP-Format, das dieser Import nicht liest.", + "That KMZ is encrypted.": "Diese KMZ ist verschlüsselt.", + "That KMZ uses a compression method this import does not read.": "Diese KMZ verwendet eine Komprimierungsmethode, die dieser Import nicht liest.", + "That KML has invalid geometry bounds.": "Diese KML enthält ungültige Geometriegrenzen.", + "That KML has invalid coordinates.": "Diese KML enthält ungültige Koordinaten.", + "That GPX file has no document.": "Diese GPX-Datei hat kein -Dokument.", + "That {format} file is not valid XML.": "Diese {format}-Datei ist kein gültiges XML.", + "That file has an unreadable image or exceeds the 16-megapixel image limit.": "Diese Datei enthält ein unlesbares Bild oder überschreitet das Bildlimit von 16 Megapixeln.", + "That KML could not be read.": "Diese KML konnte nicht gelesen werden.", + "That file has invalid point coordinates.": "Diese Datei enthält ungültige Punktkoordinaten.", + "Ground heights are not available for that file. Try again when the map has loaded.": "Für diese Datei sind keine Geländehöhen verfügbar. Erneut versuchen, wenn die Karte geladen ist.", + "That file took too long to read.": "Das Lesen dieser Datei hat zu lange gedauert.", + "Nothing to import.": "Nichts zu importieren.", + "That file is larger than {size} MiB.": "Diese Datei ist größer als {size} MiB.", + "Only GeoJSON, KML, KMZ and GPX files can be imported.": "Nur GeoJSON-, KML-, KMZ- und GPX-Dateien können importiert werden.", + "That file has nothing to draw.": "Diese Datei enthält nichts zum Zeichnen.", + "That KMZ holds no KML document.": "Diese KMZ enthält kein KML-Dokument.", + "That {format} file could not be read.": "Diese {format}-Datei konnte nicht gelesen werden.", + "That file has {count} map objects; the limit is {limit}.": "Diese Datei hat {count} Kartenobjekte; das Limit ist {limit}.", + "Ground heights could not be read. Try that file again.": "Geländehöhen konnten nicht gelesen werden. Die Datei erneut laden.", + "That file has invalid point graphics.": "Diese Datei enthält ungültige Punktgrafiken.", + "Map tiles temporarily unavailable": "Kartenkacheln vorübergehend nicht verfügbar", + "Mapped names temporarily unavailable": "Kartierte Namen vorübergehend nicht verfügbar", + "Military airfield": "Militärflugplatz", + "Naval base": "Marinestützpunkt", + "Firing range": "Schießplatz", + "Military land": "Militärgelände", + "Mapped installation": "Kartierte Anlage", + "Military area": "Militärisches Gebiet", + "flight": "Flug", + "military flight": "Militärflug", + "satellite": "Satellit", + "Draw": "Zeichnen", + "Directions": "Route", + "Recent imagery": "Aktuelle Bilddaten", + "Included in bundle": "Im Bundle enthalten", + "Missing bundle file — reimport its bundle": "Bundle-Datei fehlt – Bundle erneut importieren", + "Source configured; file checked when loaded": "Quelle konfiguriert; Datei wird beim Laden geprüft", + "Source unavailable": "Quelle nicht verfügbar", + "NORMAL": "NORMAL", + "ANIME": "ANIME", + "NOIR": "NOIR", + "ARCTIC": "ARKTIS", + "ANTARCTIC": "ANTARKTIS", + "NORTH AMERICA": "NORDAMERIKA", + "SOUTH AMERICA": "SÜDAMERIKA", + "EUROPE": "EUROPA", + "AFRICA": "AFRIKA", + "ASIA": "ASIEN", + "OCEANIA": "OZEANIEN", + "NORTHERN OCEANIC GRID": "NÖRDLICHES OZEANRASTER", + "SOUTHERN OCEANIC GRID": "SÜDLICHES OZEANRASTER", + "{state} {layers}": "{state} {layers}", + "view band|STREET": "STRASSE", + "view band|CITY": "STADT", + "view band|METRO": "GROSSRAUM", + "view band|REGIONAL": "REGION", + "view band|GLOBAL": "GLOBAL", + "N/A": "k. A.", + "{mode} {band} {locality} | {region} | ALT {alt} | WINDOW {window} | SUN {sun}° | ONA {ona}° | {zone}": "{mode} {band} {locality} | {region} | ALT {alt} | FENSTER {window} | SONNE {sun}° | ONA {ona}° | {zone}", + "NEAR {poi} ({city}) {distance}": "NAHE {poi} ({city}) {distance}", + "SECTOR {lat} {lon}": "SEKTOR {lat} {lon}", + "hemisphere|N": "N", + "hemisphere|S": "S", + "hemisphere|E": "O", + "hemisphere|W": "W", + "Provider Settings does not answer proxied requests": "Anbieter-Einstellungen beantworten keine weitergeleiteten Anfragen", + "Provider Settings is disabled while sharing is enabled": "Anbieter-Einstellungen sind deaktiviert, solange das Teilen aktiv ist", + "Provider Settings answers only the machine running the server": "Anbieter-Einstellungen antworten nur dem Rechner, auf dem der Server läuft", + "Provider Settings answers only local hostnames": "Anbieter-Einstellungen antworten nur lokalen Hostnamen", + "Provider Settings requires an exact local Origin": "Anbieter-Einstellungen erfordern einen exakt lokalen Origin", + "Unrecognized Origin refused": "Unbekannter Origin abgelehnt", + "Cross-origin requests are refused": "Cross-Origin-Anfragen werden abgelehnt", + "Content-Type must be application/json": "Content-Type muss application/json sein", + "Method not allowed": "Methode nicht erlaubt", + "Request too large": "Anfrage zu groß", + "Invalid JSON": "Ungültiges JSON", + "Body must be a JSON object of {ENV_VAR: value}": "Body muss ein JSON-Objekt der Form {ENV_VAR: value} sein", + "No keys provided": "Keine Schlüssel angegeben", + "At most {limit} keys per save": "Höchstens {limit} Schlüssel pro Speichervorgang", + "Unknown key: {name}": "Unbekannter Schlüssel: {name}", + "{name} must be a string": "{name} muss ein String sein", + "{name} is empty": "{name} ist leer", + "{name} is longer than any real key ({limit} max)": "{name} ist länger als jeder echte Schlüssel (max. {limit})", + "{name} may only contain printable characters with no spaces": "{name} darf nur druckbare Zeichen ohne Leerzeichen enthalten", + "{name} contains a character that is not valid in a key (#, quotes, $, \\, or backtick)": "{name} enthält ein Zeichen, das in einem Schlüssel ungültig ist (#, Anführungszeichen, $, \\ oder Backtick)", + "{name} is configured outside Provider Settings and can only be changed where it was set": "{name} ist außerhalb der Anbieter-Einstellungen konfiguriert und kann nur dort geändert werden, wo er gesetzt wurde", + "The key was not saved: could not restrict the credential file to your account; nothing was saved": "Der Schlüssel wurde nicht gespeichert: Die Zugangsdatei ließ sich nicht auf das eigene Benutzerkonto beschränken; nichts wurde gespeichert", + "The key was not saved: the existing configuration could not be read, so nothing was changed": "Der Schlüssel wurde nicht gespeichert: Die vorhandene Konfiguration konnte nicht gelesen werden, daher wurde nichts geändert", + "Could not write the {store} store": "Speicher {store} konnte nicht geschrieben werden", + "OAuth check failed: {detail}": "OAuth-Prüfung fehlgeschlagen: {detail}", + "Could not check ChatGPT sign-in. Try again.": "ChatGPT-Anmeldung konnte nicht geprüft werden. Erneut versuchen.", + "VOICE AUTH · CHATGPT OAUTH": "SPRACH-AUTH · CHATGPT OAUTH", + "VOICE AUTH · API KEY": "SPRACH-AUTH · API-SCHLÜSSEL", + "USE API KEY": "API-SCHLÜSSEL VERWENDEN", + "USE CHATGPT OAUTH": "CHATGPT OAUTH VERWENDEN", + "Use OPENAI_API_KEY for the next cloud voice session": "OPENAI_API_KEY für die nächste Cloud-Sprachsitzung verwenden", + "Use the signed-in local ChatGPT/Codex OAuth session for the next cloud voice session": "Die angemeldete lokale ChatGPT/Codex-OAuth-Sitzung für die nächste Cloud-Sprachsitzung verwenden", + "Metered — a billing-enabled account": "Kostenpflichtig – Konto mit aktivierter Abrechnung", + "Optional paid token — public access works without one": "Optionales kostenpflichtiges Token – öffentlicher Zugang funktioniert auch ohne", + "Free key — register, paste, done": "Kostenloser Schlüssel – registrieren, einfügen, fertig", + "browser-side": "browserseitig", + "This key runs in the browser by design — restrict it at the provider (see SECURITY.md)": "Dieser Schlüssel läuft absichtlich im Browser – beim Anbieter einschränken (siehe SECURITY.md)", + "configured externally": "extern konfiguriert", + "Supplied by your environment, Keychain, or launcher — change it where it was set": "Aus Umgebung, Schlüsselbund oder Launcher – dort ändern, wo er gesetzt wurde", + "MANAGE ↗": "VERWALTEN ↗", + "GET KEY ↗": "SCHLÜSSEL HOLEN ↗", + "{envVar} saved — paste to replace": "{envVar} gespeichert – zum Ersetzen einfügen", + "paste {envVar}": "{envVar} einfügen", + "unsaved provider keys": "ungespeicherte Anbieter-Schlüssel", + "Project keys: {status}": "Projektschlüssel: {status}", + "Cloud voice will use OPENAI_API_KEY on the next session.": "Die Cloud-Sprachsteuerung verwendet ab der nächsten Sitzung OPENAI_API_KEY.", + "Checking local ChatGPT OAuth sign-in…": "Lokale ChatGPT-OAuth-Anmeldung wird geprüft…", + "Could not check ChatGPT sign-in.": "ChatGPT-Anmeldung konnte nicht geprüft werden.", + "ChatGPT OAuth selected for cloud voice. Your API key stays saved and available.": "ChatGPT OAuth für die Cloud-Sprachsteuerung gewählt. Der API-Schlüssel bleibt gespeichert und verfügbar.", + "ChatGPT sign-in expired. Opening sign-in again…": "ChatGPT-Anmeldung abgelaufen. Anmeldung wird erneut geöffnet…", + "Opening ChatGPT sign-in in your browser…": "ChatGPT-Anmeldung wird im Browser geöffnet…", + "Could not start ChatGPT sign-in on this machine.": "ChatGPT-Anmeldung konnte auf diesem Rechner nicht gestartet werden.", + "Finish ChatGPT sign-in in the browser. Waiting for it to complete…": "ChatGPT-Anmeldung im Browser abschließen. Warten auf Abschluss…", + "ChatGPT sign-in timed out. Click {button} to try again.": "Zeitüberschreitung bei der ChatGPT-Anmeldung. Zum erneuten Versuch auf {button} klicken.", + "ChatGPT sign-in complete. OAuth will be used for the next cloud voice session.": "ChatGPT-Anmeldung abgeschlossen. OAuth wird für die nächste Cloud-Sprachsitzung verwendet.", + "The photorealistic 3D planet, place search + Street View": "Der fotorealistische 3D-Planet, Ortssuche + Street View", + "Voice control — API key or ChatGPT OAuth": "Sprachsteuerung – API-Schlüssel oder ChatGPT OAuth", + "Gemini Live voice control": "Sprachsteuerung mit Gemini Live", + "Street-level imagery and coverage in the Street Level layer. Free: register an app in the Mapillary developer dashboard and paste its Client Token": "Bilder und Abdeckung auf Straßenebene in der Ebene „Straßenebene“. Kostenlos: im Mapillary-Entwickler-Dashboard eine App registrieren und deren Client Token einfügen", + "Public space-missions access needs no key. Leave blank for standard use; paid tokens only raise request limits.": "Öffentlicher Zugang zu Raummissionen braucht keinen Schlüssel. Für normale Nutzung leer lassen; kostenpflichtige Tokens erhöhen nur die Anfragelimits.", + "Air quality request failed": "Luftqualitätsabfrage fehlgeschlagen", + "No readings available": "Keine Messwerte verfügbar", + "No current readings": "Keine aktuellen Messwerte", + "{count} stations reporting": { + "one": "{count} Station meldet", + "other": "{count} Stationen melden" + }, + "Highest air-quality readings in view": "Schlechteste Luftqualitätswerte im Bild", + "Low": "Niedrig", + "Low health risk": "Geringes Gesundheitsrisiko", + "Moderate": "Mäßig", + "Moderate health risk": "Mäßiges Gesundheitsrisiko", + "High": "Hoch", + "High health risk": "Hohes Gesundheitsrisiko", + "Very High": "Sehr hoch", + "Very High health risk": "Sehr hohes Gesundheitsrisiko", + "Air quality": "Luftqualität", + "Canada only · ECCC AQHI network": "Nur Kanada · ECCC-AQHI-Messnetz", + "ECCC request timed out": "Zeitüberschreitung bei der ECCC-Abfrage", + "Malformed AQHI station response": "Fehlerhafte Antwort der AQHI-Stationen", + "No AQHI stations available": "Keine AQHI-Stationen verfügbar", + "Malformed AQHI observation response": "Fehlerhafte Antwort der AQHI-Beobachtungen", + "Camera source temporarily unavailable": "Kameraquelle vorübergehend nicht verfügbar", + "Mapped camera locations": "Kartierte Kamerastandorte", + "SHOW NEAREST": "NÄCHSTGELEGENE ZEIGEN", + "Stop following the current object before navigating to a camera": "Vor dem Wechsel zu einer Kamera das Verfolgen des aktuellen Objekts beenden", + "Move to the nearest loaded camera and show its details": "Zur nächstgelegenen geladenen Kamera wechseln und Details zeigen", + "Camera badges": "Kamera-Markierungen", + "Cyan cameras turn coral when selected. Wedges illustrate mapped direction, not measured coverage. Nearby cameras may be outside the screen.": "Cyanfarbene Kameras werden bei Auswahl korallenrot. Keile zeigen die kartierte Blickrichtung, keine gemessene Abdeckung. Kameras in der Nähe können außerhalb des Bildschirms liegen.", + "{count} nearby · {onScreen} on screen": { + "one": "{count} in der Nähe · {onScreen} im Bild", + "other": "{count} in der Nähe · {onScreen} im Bild" + }, + "{count} nearby": { + "one": "{count} in der Nähe", + "other": "{count} in der Nähe" + }, + "retrying mapped ALPR cameras": "kartierte ALPR-Kameras: neuer Versuch", + "loading mapped ALPR cameras": "kartierte ALPR-Kameras werden geladen", + "No ALPR data for this area — US and Canada only": "Keine ALPR-Daten für dieses Gebiet – nur USA und Kanada", + "Zoom in to load mapped cameras": "Hineinzoomen, um kartierte Kameras zu laden", + "Showing cached locations": "Standorte aus dem Cache", + "Coverage limited — zoom in": "Abdeckung begrenzt – hineinzoomen", + "No ALPR data for this area": "Keine ALPR-Daten für dieses Gebiet", + "None on screen — nearby cameras are outside the view": "Keine im Bild – Kameras in der Nähe liegen außerhalb der Ansicht", + "ALPR camera": "ALPR-Kamera", + "Camera coverage unavailable": "Kameraabdeckung nicht verfügbar", + "OpenStreetMap · community mapped": "OpenStreetMap · von der Community kartiert", + "Overpass returned an incomplete camera response": "Overpass hat eine unvollständige Kamera-Antwort geliefert", + "At least {count}": "Mindestens {count}", + "Named areas ({n})": "Benannte Gebiete ({n})", + "{distance} FLIGHT / VESSEL WINDOW": "{distance} FLUG-/SCHIFFSFENSTER", + "mapped matches within {km} km of the subject": "kartierte Treffer im Umkreis von {km} km um das Objekt", + "none mapped within {km} km; not a complete 250 km survey": "keine kartierten Treffer im Umkreis von {km} km; keine vollständige 250-km-Erfassung", + "mapped matches from the loaded viewport": "kartierte Treffer aus dem geladenen Bildausschnitt", + "viewport feed is not a complete 250 km survey": "Feed des Bildausschnitts ist keine vollständige 250-km-Erfassung", + "GBFS fetch error": "GBFS-Abruffehler", + "FOV {value}° (DATASHEET)": "FOV {value}° (DATENBLATT)", + "SPEC FOV {span}° (CAPABILITY)": "SPEZ. FOV {span}° (BEREICH)", + "{spec} · SPECS: CCTV-DATABASE.COM": "{spec} · SPEZ.: CCTV-DATABASE.COM", + "Atlantic and eastern/central North Pacific; not worldwide cyclone coverage.": "Atlantik und östlicher/zentraler Nordpazifik; keine weltweite Abdeckung von Wirbelstürmen.", + "Cyclone advisories unavailable": "Wirbelsturm-Lageberichte nicht verfügbar", + "Cyclone data unavailable": "Wirbelsturmdaten nicht verfügbar", + "Potential tropical cyclone": "Potenzieller tropischer Wirbelsturm", + "Post-tropical cyclone": "Posttropischer Wirbelsturm", + "Hurricane": "Hurrikan", + "Tropical storm": "Tropischer Sturm", + "Tropical depression": "Tropisches Tiefdruckgebiet", + "Subtropical storm": "Subtropischer Sturm", + "Subtropical depression": "Subtropisches Tiefdruckgebiet", + "Track and cone match this advisory": "Zugbahn und Kegel passen zu diesem Lagebericht", + "Track/cone awaiting advisory {number}": "Zugbahn/Kegel warten auf Lagebericht {number}", + "Track/cone unavailable": "Zugbahn/Kegel nicht verfügbar", + "Cached advisory · stale source": "Lagebericht aus dem Cache · Quelle veraltet", + "Loading advisories…": "Lageberichte werden geladen…", + "{count} active storms": { + "one": "{count} aktiver Sturm", + "other": "{count} aktive Stürme" + }, + "Position as of {time}": "Position, Stand {time}", + "Maximum sustained wind: {wind} · Pressure: {pressure}": "Maximaler Dauerwind: {wind} · Luftdruck: {pressure}", + "{name} · {classification} · Advisory {number} · issued {time}": "{name} · {classification} · Lagebericht {number} · ausgegeben {time}", + "No active NHC/CPHC systems": "Keine aktiven NHC/CPHC-Systeme", + "Advisories unavailable": "Lageberichte nicht verfügbar", + "Cyclones · NHC / CPHC": "Wirbelstürme · NHC / CPHC", + "Atlantic · E/C Pacific": "Atlantik · östl./zentr. Pazifik", + "{storms} · {name} selected": "{storms} · {name} ausgewählt", + "Official advisory ↗": "Offizieller Lagebericht ↗", + "Active NHC and CPHC cyclone advisories": "Aktive Wirbelsturm-Lageberichte von NHC und CPHC", + "Wind unavailable": "Wind nicht verfügbar", + "Advisory center / forecast track": "Zentrum laut Lagebericht / Vorhersagebahn", + "Center-track uncertainty cone": "Unsicherheitskegel der Zentrumsbahn", + "Select a storm on the map, or choose a storm in the list to select it and move the camera. Click empty map space to clear the selection. NOAA NHC/CPHC advisory context. The cone describes forecast center-track uncertainty, not storm size or the full hazard area. Forecast point labels are source lead hours, not times computed from advisory issuance. Geometry follows the surface; height is not weather altitude. Consult the official advisory.": "Einen Sturm auf der Karte auswählen oder in der Liste wählen, um ihn auszuwählen und die Kamera dorthin zu bewegen. Auf eine leere Kartenstelle klicken, um die Auswahl aufzuheben. Kontext aus Lageberichten von NOAA NHC/CPHC. Der Kegel beschreibt die Unsicherheit der vorhergesagten Zentrumsbahn, nicht die Sturmgröße oder das gesamte Gefahrengebiet. Die Beschriftungen der Vorhersagepunkte sind Vorlaufstunden der Quelle, keine aus dem Ausgabezeitpunkt berechneten Zeiten. Die Geometrie folgt der Oberfläche; die Darstellungshöhe hat keine meteorologische Bedeutung. Den offiziellen Lagebericht beachten.", + "DRIVE": "AUTO", + "Drive": "Auto", + "WALK": "ZU FUSS", + "Walk": "Zu Fuß", + "BIKE": "RAD", + "Bike": "Fahrrad", + "rate limited": "Anfragelimit erreicht", + "routing service is rate limited": "Routing-Dienst hat das Anfragelimit erreicht", + "route leg too long": "Routenabschnitt zu lang", + "route too long": "Route zu lang", + "{error} — try again in a moment": "{error} – gleich erneut versuchen", + "Routing is rate limited — try again in a moment": "Routing-Anfragelimit erreicht – gleich erneut versuchen", + "Routing unavailable (HTTP {status})": "Routing nicht verfügbar (HTTP {status})", + "Routing unavailable": "Routing nicht verfügbar", + "direction|left": "links", + "direction|right": "rechts", + "direction|slightly left": "leicht links", + "direction|slightly right": "leicht rechts", + "direction|sharply left": "scharf links", + "direction|sharply right": "scharf rechts", + "direction|straight": "geradeaus", + "direction|around": "in Gegenrichtung", + "fork direction|left": "links", + "fork direction|right": "rechts", + "fork direction|slightly left": "leicht links", + "fork direction|slightly right": "leicht rechts", + "fork direction|sharply left": "scharf links", + "fork direction|sharply right": "scharf rechts", + "fork direction|straight": "geradeaus", + "lane direction|left": "linke", + "lane direction|right": "rechte", + "lane direction|slightly left": "leicht nach links führende", + "lane direction|slightly right": "leicht nach rechts führende", + "lane direction|sharply left": "scharf nach links führende", + "lane direction|sharply right": "scharf nach rechts führende", + "lane direction|straight": "geradeaus führende", + "ordinal|{number}st": "{number}.", + "ordinal|{number}nd": "{number}.", + "ordinal|{number}rd": "{number}.", + "ordinal|{number}th": "{number}.", + "Head out on {road}": "Start auf {road}", + "Head out": "Start", + "Arrive at your destination, on the left": "Ziel erreicht, es liegt links", + "Arrive at your destination, on the right": "Ziel erreicht, es liegt rechts", + "Arrive at your destination": "Ziel erreicht", + "Make a U-turn onto {road}": "Wenden und weiter auf {road}", + "Make a U-turn": "Wenden", + "Continue straight onto {road}": "Geradeaus weiter auf {road}", + "Continue straight": "Geradeaus weiter", + "Turn {direction} onto {road}": "Auf {road} {direction} abbiegen", + "Turn {direction}": "Hier {direction} abbiegen", + "Turn onto {road}": "Auf {road} abbiegen", + "maneuver|Turn": "Abbiegen", + "Continue {direction} onto {road}": "Weiter {direction} auf {road}", + "Continue {direction}": "Weiter {direction}", + "At the end of the road, turn {direction} onto {road}": "Am Ende der Straße {direction} auf {road} abbiegen", + "At the end of the road, turn {direction}": "Am Ende der Straße {direction} abbiegen", + "At the end of the road, continue onto {road}": "Am Ende der Straße weiter auf {road}", + "At the end of the road, continue": "Am Ende der Straße weiter", + "Keep {direction} at the fork onto {road}": "An der Gabelung {direction} weiter auf {road}", + "Keep {direction} at the fork": "An der Gabelung {direction} weiter", + "Continue at the fork onto {road}": "An der Gabelung weiter auf {road}", + "Continue at the fork": "An der Gabelung weiter", + "Merge {direction} onto {road}": "Auf {road} {direction} einfädeln", + "Merge {direction}": "Hier {direction} einfädeln", + "Merge onto {road}": "Auf {road} einfädeln", + "maneuver|Merge": "Einfädeln", + "Take the ramp on the {direction} onto {road}": "Die Auffahrt {direction} auf {road} nehmen", + "Take the ramp on the {direction}": "Die Auffahrt {direction} nehmen", + "Take the ramp {direction} onto {road}": "Die Auffahrt {direction} auf {road} nehmen", + "Take the ramp {direction}": "Die Auffahrt {direction} nehmen", + "Take the ramp onto {road}": "Die Auffahrt auf {road} nehmen", + "Take the ramp": "Die Auffahrt nehmen", + "Take the exit on the {direction} onto {road}": "Die Ausfahrt {direction} auf {road} nehmen", + "Take the exit on the {direction}": "Die Ausfahrt {direction} nehmen", + "Take the exit {direction} onto {road}": "Die Ausfahrt {direction} auf {road} nehmen", + "Take the exit {direction}": "Die Ausfahrt {direction} nehmen", + "Take the exit onto {road}": "Die Ausfahrt auf {road} nehmen", + "Take the exit": "Die Ausfahrt nehmen", + "At the roundabout, take the {exit} exit onto {road}": "Im Kreisverkehr die {exit} Ausfahrt auf {road} nehmen", + "At the roundabout, take the {exit} exit": "Im Kreisverkehr die {exit} Ausfahrt nehmen", + "Enter the roundabout onto {road}": "In den Kreisverkehr auf {road} einfahren", + "Enter the roundabout": "In den Kreisverkehr einfahren", + "At the roundabout, turn {direction} onto {road}": "Im Kreisverkehr {direction} auf {road} abbiegen", + "At the roundabout, turn {direction}": "Im Kreisverkehr {direction} abbiegen", + "At the roundabout, continue onto {road}": "Im Kreisverkehr weiter auf {road}", + "At the roundabout, continue": "Im Kreisverkehr weiter", + "Exit the roundabout onto {road}": "Den Kreisverkehr auf {road} verlassen", + "Exit the roundabout": "Den Kreisverkehr verlassen", + "Use the {direction} lane onto {road}": "Die {direction} Spur auf {road} benutzen", + "Use the {direction} lane": "Die {direction} Spur benutzen", + "Continue onto {road}": "Weiter auf {road}", + "maneuver|Continue": "Weiter", + "{mode} — reroute for {modeLower}": "{mode} – Route im Modus „{modeLower}“ neu berechnen", + "CLICK MAP": "KARTE ANKLICKEN", + "SET A": "A SETZEN", + "Click a spot on the globe to place A (click again to cancel)": "Auf eine Stelle des Globus klicken, um A zu setzen (erneut klicken zum Abbrechen)", + "Then click the globe to place the start": "Dann auf den Globus klicken, um den Start zu setzen", + "SET B": "B SETZEN", + "Click a spot on the globe to place B (click again to cancel)": "Auf eine Stelle des Globus klicken, um B zu setzen (erneut klicken zum Abbrechen)", + "Then click the globe to place the destination": "Dann auf den Globus klicken, um das Ziel zu setzen", + "Swap A and B": "A und B tauschen", + "FLY ···": "FLIEGEN ···", + "FLYING": "IM FLUG", + "FLY": "FLIEGEN", + "Fly the camera along the route": "Kamera entlang der Route fliegen lassen", + "Place A and B first": "Zuerst A und B setzen", + "action|CLEAR": "LÖSCHEN", + "Remove the route and both markers": "Route und beide Markierungen entfernen", + "press Escape twice to leave Draw": "zweimal Esc drücken, um Zeichnen zu verlassen", + "{name} is active — turn {name} off, then set {endpoint}": "{name} ist aktiv – {name} ausschalten, dann {endpoint} setzen", + "{name} is active — {leave}, then set {endpoint}": "{name} ist aktiv – {leave}, dann {endpoint} setzen", + "Only the first {count} turns of this route are shown": "Nur die ersten {count} Abbiegungen dieser Route werden gezeigt", + "Turn-by-turn directions": "Abbiegehinweise", + "Another map tool is using clicks — close it, then SET A again": "Ein anderes Kartenwerkzeug nutzt die Klicks – es schließen, dann erneut A SETZEN", + "Routing…": "Route wird berechnet…", + "No route found": "Keine Route gefunden", + "{distance} · {duration} · {mode} · first {count} turns": "{distance} · {duration} · {mode} · erste {count} Abbiegungen", + "{distance} · {duration} · {mode}": "{distance} · {duration} · {mode}", + "Click the globe to place {endpoint}": "Auf den Globus klicken, um {endpoint} zu setzen", + "SET B, then click the globe": "B SETZEN, dann auf den Globus klicken", + "SET A, then click the globe": "A SETZEN, dann auf den Globus klicken", + "Step {step} of {total} · then {leg}": "Schritt {step} von {total} · dann {leg}", + "Step {step} of {total}": "Schritt {step} von {total}", + "Then: {instruction}": "Nächster Schritt: {instruction}", + "No route found between A and B": "Keine Route zwischen A und B gefunden", + "Routing failed: {error}": "Routing fehlgeschlagen: {error}", + "Routing timed out": "Zeitüberschreitung beim Routing", + "Routing unavailable — no response from the routing service": "Routing nicht verfügbar – keine Antwort vom Routing-Dienst", + "Earthquake source unavailable": "Erdbebenquelle nicht verfügbar", + "live feed unavailable": "Live-Feed nicht verfügbar", + "{value}°N": "{value}°N", + "{value}°S": "{value}°S", + "{value}°E": "{value}°O", + "{value}°W": "{value}°W", + "fire confidence|high": "hoch", + "fire confidence|nominal": "mittel", + "fire confidence|low": "niedrig", + "age|<1h": "<1 h", + "age|{count}h": "{count} h", + "age|{count}d": "{count} T", + "Source snapshot time unavailable": "Zeitpunkt des Quell-Snapshots nicht verfügbar", + "Live data unavailable": "Live-Daten nicht verfügbar", + "Intel Picture": "Lagebild", + "Kinetic 30d": "Kinetisch 30 T", + "Border Crisis": "Grenzkrise", + "Iran Picture": "Lagebild Iran", + "Iran Kinetic": "Iran kinetisch", + "Yemen FLOT": "Jemen Frontlinie", + "Latest Drop": "Neueste Ausgabe", + "GhostMaps file could not be read": "GhostMaps-Datei konnte nicht gelesen werden", + "{count} source links ↗ · click card to open the first": { + "one": "{count} Quelllink ↗ · Eintrag anklicken zum Öffnen", + "other": "{count} Quelllinks ↗ · Eintrag anklicken, um den ersten zu öffnen" + }, + "S2 · {name}": "S2 · {name}", + "Report": "Bericht", + "Open the source for {title}": "Quelle für {title} öffnen", + "GhostMaps catalog unavailable": "GhostMaps-Katalog nicht verfügbar", + "LIVE · GitHub {commit} · {message} · checked every {seconds}s": "LIVE · GitHub {commit} · {message} · Prüfung alle {seconds} s", + "GitHub {commit} · {message} · reconnecting for live updates": "GitHub {commit} · {message} · Verbindung für Live-Updates wird wiederhergestellt", + "Connecting to the S2 GhostMaps repository…": "Verbindung zum S2-GhostMaps-Repository…", + "{count} placemarks": { + "one": "{count} Ortsmarke", + "other": "{count} Ortsmarken" + }, + "S2 Underground publishes these KMZ products to GitHub. New uploads are detected within seconds and replace the previous edition automatically. Click a marker for its report.": "S2 Underground veröffentlicht diese KMZ-Produkte auf GitHub. Neue Uploads werden innerhalb von Sekunden erkannt und ersetzen automatisch die vorherige Ausgabe. Für den Bericht eine Markierung anklicken.", + "Malformed GhostMaps catalog": "Fehlerhafter GhostMaps-Katalog", + "Low-accuracy score < 2%": "Ungenauigkeitswert < 2 %", + "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 30 min": "Regionale Stichprobe · Radius 250 NM · Zellen aus besuchten Ansichten über 30 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.", + "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.": "Ungenauigkeitswert = 100 × max(0, schlecht − 1) / gesamt: Ein Luftfahrzeug wird abgezogen, das ist also nicht der reine Anteil. Zellen brauchen {aircraft} Luftfahrzeuge über {minutes} Minuten. Anomalien der Navigationsintegrität sind kein Nachweis von Jamming oder Spoofing.", + "GNSS snapshot needs a view anchor": "GNSS-Snapshot braucht einen Ansichtsanker", + "GNSS feed rate limited (HTTP 429)": "GNSS-Feed hat das Anfragelimit erreicht (HTTP 429)", + "Malformed GNSS integrity snapshot": "Fehlerhafter GNSS-Integritäts-Snapshot", + "GNSS integrity snapshot has no observation time": "GNSS-Integritäts-Snapshot ohne Beobachtungszeit", + "installation class|AIRFIELD": "FLUGPLATZ", + "installation class|NAVAL BASE": "MARINESTÜTZPUNKT", + "installation class|RANGE": "SCHIESSPLATZ", + "installation class|MILITARY LAND": "MILITÄRGELÄNDE", + "installation class|BASE": "STÜTZPUNKT", + "installation class|BARRACKS": "KASERNE", + "installation class|TRAINING AREA": "ÜBUNGSPLATZ", + "installation class|PLACES CANDIDATE": "AUS GOOGLE PLACES", + "installation class|INSTALLATION": "ANLAGE", + "No mapped sites within {km} km of the contact": "Keine kartierten Standorte im Umkreis von {km} km um den Kontakt", + "{count} mapped sites within {km} km of the contact": { + "one": "{count} kartierter Standort im Umkreis von {km} km um den Kontakt", + "other": "{count} kartierte Standorte im Umkreis von {km} km um den Kontakt" + }, + "No mapped sites in view": "Keine kartierten Standorte im Bild", + "{count} mapped sites in view": { + "one": "{count} kartierter Standort im Bild", + "other": "{count} kartierte Standorte im Bild" + }, + "Too many mapped sites in view to list them all": "Zu viele kartierte Standorte im Bild, um alle aufzulisten", + "Installation context unavailable": "Kontext der Anlage nicht verfügbar", + "Unknown": "Unbekannt", + "Selected Space Mission": "Gewählte Raummission", + "Malformed launch snapshot": "Fehlerhafter Start-Snapshot", + "Heard by your receiver": "Vom eigenen Empfänger empfangen", + "1090 MHz": "1090 MHz", + "978 MHz UAT": "978 MHz UAT", + "browser SDR": "Browser-SDR", + "decoder feed": "Decoder-Feed", + "Your RTL-SDR receiver and decoder feed": "Eigener RTL-SDR-Empfänger und Decoder-Feed", + "Your decoder feed": "Eigener Decoder-Feed", + "Your RTL-SDR receiver": "Eigener RTL-SDR-Empfänger", + "NO CALLSIGN": "KEIN RUFZEICHEN", + "ALT {altitude} FT · GS {speed} KT · TRK {track}": "ALT {altitude} FT · GS {speed} KN · TRK {track}", + "V/S {rate}": "V/S {rate}", + "V/S {rate} FPM": "V/S {rate} FT/MIN", + "POSITION {age}": "POSITION {age}", + "POSITION {seconds} S AGO": "POSITION VOR {seconds} S", + "{count} MSGS": { + "one": "{count} NACHR.", + "other": "{count} NACHR." + }, + "Decoder feeds": "Decoder-Feeds", + "WebUSB + decoder feeds": "WebUSB + Decoder-Feeds", + "opening receiver": "Empfänger wird geöffnet", + "checking decoder feeds": "Decoder-Feeds werden geprüft", + "WebUSB needs desktop Chrome or Edge": "WebUSB braucht Chrome oder Edge auf dem Desktop", + "connect a receiver in Radio": "Empfänger unter Radio verbinden", + "receiver is in FM mode": "Empfänger ist im FM-Modus", + "listening": "empfängt", + "USB error": "USB-Fehler", + "${amount}K": "{amount} Tsd. $", + "${amount}M": "{amount} Mio. $", + "${amount}B": "{amount} Mrd. $", + "fire cause|Human": "Mensch", + "fire cause|Natural": "Natürlich", + "fire cause|Undetermined": "Ungeklärt", + "fire cause|Unknown": "Unbekannt", + "fire behavior|Minimal": "Minimal", + "fire behavior|Moderate": "Mäßig", + "fire behavior|Active": "Aktiv", + "fire behavior|Extreme": "Extrem", + "Type {level} Incident": "Einsatz Typ {level}", + "age|{count}m": "{count} min", + "{acres} ac": { + "one": "{acres} Acre", + "other": "{acres} Acres" + }, + "{percent}% contained": "{percent} % eingedämmt", + "containment unknown": "Eindämmung unbekannt", + "{cause} cause": "Ursache: {cause}", + "{personnel} personnel": { + "one": "{personnel} Einsatzkraft", + "other": "{personnel} Einsatzkräfte" + }, + "{county} County": "{county} County", + "{cost} to date": "{cost} bisher", + "discovered {age} ago": "entdeckt vor {age}", + "updated {age} ago": "aktualisiert vor {age}", + "part of {complex}": "Teil von {complex}", + "InciWeb ↗ · click card to open": "InciWeb ↗ · Eintrag anklicken zum Öffnen", + "FIRE · {name}": "BRAND · {name}", + "Unnamed incident": "Unbenannter Einsatz", + "Open {title} on InciWeb": "{title} auf InciWeb öffnen", + "Perimeter source unavailable": "Quelle der Brandgrenzen nicht verfügbar", + "Not contained or unknown": "Nicht eingedämmt oder unbekannt", + "Under 50% contained": "Unter 50 % eingedämmt", + "50–99% contained": "50–99 % eingedämmt", + "Fully contained": "Vollständig eingedämmt", + "Colour shows reported containment. Perimeters are simplified to about 100 m.": "Die Farbe zeigt die gemeldete Eindämmung. Brandgrenzen sind auf etwa 100 m vereinfacht.", + "Malformed perimeter snapshot": "Fehlerhafter Brandgrenzen-Snapshot", + "radio tag|All": "Alle", + "Directory refresh degraded; showing the previous station catalog.": "Verzeichnisaktualisierung eingeschränkt; der vorherige Senderkatalog wird gezeigt.", + "Radio directory coverage is degraded.": "Abdeckung des Radioverzeichnisses ist eingeschränkt.", + "Directory refresh failed; showing the previous station catalog.": "Verzeichnisaktualisierung fehlgeschlagen; der vorherige Senderkatalog wird gezeigt.", + "Radio directory is temporarily unavailable.": "Radioverzeichnis vorübergehend nicht verfügbar.", + "radio|NEWS": "NEWS", + "radio|WEATHER": "WETTER", + "radio|OTHER": "SONST.", + "Broadcaster stream is unavailable or blocked by the browser.": "Der Stream des Senders ist nicht verfügbar oder wird vom Browser blockiert.", + "Playback requires a direct click or tap.": "Die Wiedergabe muss direkt per Klick oder Antippen gestartet werden.", + "Broadcaster stream could not be started.": "Der Stream des Senders konnte nicht gestartet werden.", + "overview": "Übersicht", + "cloud unknown": "Bewölkung unbekannt", + "{percent}% scene cloud": "{percent} % Bewölkung in der Szene", + "{min}–{max}% scene cloud": "{min}–{max} % Bewölkung in der Szene", + "Comparison in use by another scene": "Vergleich wird von einer anderen Szene verwendet", + "Esri map unavailable · no swipe": "Esri-Karte nicht verfügbar · kein Bildvergleich", + "Swipe needs a globe map": "Bildvergleich braucht eine Globuskarte", + "Imagery stays on Esri · CLEAR to use Google 3D": "Bilder bleiben auf Esri · LÖSCHEN, um Google 3D zu verwenden", + "Imagery catalog unavailable": "Bildkatalog nicht verfügbar", + "Catalog truncated · a newer clear day may exist": "Katalog gekürzt · ein neuerer wolkenfreier Tag ist möglich", + "{label} catalog unavailable · {detail}": "{label}-Katalog nicht verfügbar · {detail}", + "Daily overview for today may still be empty until the pass": "Die tägliche Übersicht für heute kann bis zum Überflug noch leer sein", + "Box and images cleared": "Bereich und Bilder gelöscht", + "MORE DETAIL · 30 m": "MEHR DETAILS · 30 m", + "Sentinel-2 and Landsat via HLS, 30 m, every 2–6 days": "Sentinel-2 und Landsat über HLS, 30 m, alle 2–6 Tage", + "DAILY OVERVIEW · 250 m": "TÄGLICHE ÜBERSICHT · 250 m", + "VIIRS daily mosaic, 250 m, follows broad changes; little detail under 25 km": "VIIRS-Tagesmosaik, 250 m, zeigt großräumige Veränderungen; kaum Details unter 25 km", + "Sentinel-2": "Sentinel-2", + "Sentinel-2 via HLS": "Sentinel-2 über HLS", + "Landsat 8/9": "Landsat 8/9", + "Landsat 8/9 via HLS": "Landsat 8/9 über HLS", + "Daily overview": "Tägliche Übersicht", + "Box edges must be finite": "Die Ränder des Bereichs müssen endlich sein", + "Longitudes must be within ±180°": "Längengrade müssen innerhalb von ±180° liegen", + "Select one side of the dateline": "Eine Seite der Datumsgrenze wählen", + "Box must enclose an area": "Der Bereich muss eine Fläche umschließen", + "CelesTrak unreachable": "CelesTrak nicht erreichbar", + "DOCKED · {name} · +{extra}": "ANGEDOCKT · {name} · +{extra}", + "DOCKED · {name}": "ANGEDOCKT · {name}", + "{name} needs the Google 3D map: choose it under MAP SOURCE": "{name} braucht die Google-3D-Karte: unter KARTENQUELLE auswählen", + "zoom in to a street to open {name}": "auf eine Straße hineinzoomen, um {name} zu öffnen", + "loading coverage...": "Abdeckung wird geladen...", + "No street-level imagery within {radius} m of that point": "Keine Bilder auf Straßenebene im Umkreis von {radius} m um diesen Punkt", + "No street-level imagery within {radius} m of the view centre": "Keine Bilder auf Straßenebene im Umkreis von {radius} m um die Bildmitte", + "Zoom in to see street-level coverage": "Hineinzoomen, um die Abdeckung auf Straßenebene zu sehen", + "Point the camera at the ground for street-level coverage": "Kamera auf den Boden richten, um die Abdeckung auf Straßenebene zu sehen", + "Coverage tile failed": "Abdeckungskachel fehlgeschlagen", + "The photo could not be loaded": "Das Foto konnte nicht geladen werden", + "{name} photo. ← and → step along the sequence. On a 360° photo, drag or use Shift+← →, ↑ ↓ to look around, and + − to zoom": "{name}-Foto. ← und → blättern durch die Sequenz. Bei einem 360°-Foto ziehen oder Umschalt+← →, ↑ ↓ zum Umsehen und + − zum Zoomen verwenden", + "{name} street-level photo": "{name}-Foto auf Straßenebene", + "Previous photo in this sequence (←)": "Vorheriges Foto dieser Sequenz (←)", + "Next photo in this sequence (→)": "Nächstes Foto dieser Sequenz (→)", + "Aim the middle of the view at a street to look for imagery there": "Die Bildmitte auf eine Straße richten, um dort nach Bildern zu suchen", + "KEY REJECTED": "SCHLÜSSEL ABGELEHNT", + "GOOGLE_MAPS_API_KEY is not set": "GOOGLE_MAPS_API_KEY ist nicht gesetzt", + "The Google Maps JavaScript API could not load": "Die Google Maps JavaScript API konnte nicht geladen werden", + "Google Street View": "Google Street View", + "GOOGLE STREET VIEW": "GOOGLE STREET VIEW", + "Google refused GOOGLE_MAPS_API_KEY for Street View — check that the Maps JavaScript API is enabled for it and this site is an allowed referrer": "Google hat GOOGLE_MAPS_API_KEY für Street View abgelehnt – prüfen, ob die Maps JavaScript API dafür aktiviert und diese Website als Referrer zugelassen ist", + "Street View did not answer — check the network, and that the Maps JavaScript API is enabled for GOOGLE_MAPS_API_KEY": "Street View antwortet nicht – Netzwerk prüfen und ob die Maps JavaScript API für GOOGLE_MAPS_API_KEY aktiviert ist", + "click a street to open Street View": "auf eine Straße klicken, um Street View zu öffnen", + "Street View has no imagery for this place": "Street View hat keine Bilder für diesen Ort", + "KartaView has no position for this photo": "KartaView hat keine Position für dieses Foto", + "The KartaView photo could not be loaded": "Das KartaView-Foto konnte nicht geladen werden", + "zoom in to see KartaView coverage": "hineinzoomen, um die KartaView-Abdeckung zu sehen", + "no KartaView photos in view": "keine KartaView-Fotos im Bild", + "KartaView is rate-limiting requests — they resume on their own": "KartaView begrenzt die Anfragen – sie laufen von selbst weiter", + "KartaView’s photos here are archived and cannot be shown": "Die KartaView-Fotos hier sind archiviert und können nicht angezeigt werden", + "KartaView did not filter this area in time — move the map to ask again": "KartaView hat dieses Gebiet nicht rechtzeitig gefiltert – Karte verschieben, um erneut anzufragen", + "None of KartaView’s photos here pass the imagery filter": "Keines der KartaView-Fotos hier passt zum Bildfilter", + "KartaView did not answer in time — try again": "KartaView hat nicht rechtzeitig geantwortet – erneut versuchen", + "KartaView could not be reached — check the network": "KartaView ist nicht erreichbar – Netzwerk prüfen", + "KartaView sent an answer that could not be read": "KartaView hat eine unlesbare Antwort gesendet", + "KartaView is rate-limiting requests": "KartaView begrenzt die Anfragen", + "KartaView sent too much data": "KartaView hat zu viele Daten gesendet", + "KartaView is rate-limiting requests — try again in a moment": "KartaView begrenzt die Anfragen – gleich erneut versuchen", + "KartaView has no such photo": "KartaView hat dieses Foto nicht", + "Mapillary rejected MAPILLARY_CLIENT_TOKEN — replace it in Provider Settings": "Mapillary hat MAPILLARY_CLIENT_TOKEN abgelehnt – in den Anbieter-Einstellungen ersetzen", + "Coverage requests are being rate-limited — they resume on their own": "Abdeckungsanfragen werden begrenzt – sie laufen von selbst weiter", + "The Panoramax photo could not be loaded": "Das Panoramax-Foto konnte nicht geladen werden", + "Panoramax is busy — coverage resumes on its own": "Panoramax ist ausgelastet – die Abdeckung lädt von selbst weiter", + "no Panoramax photos in view": "keine Panoramax-Fotos im Bild", + "no Panoramax photos here match the filters": "keine Panoramax-Fotos hier passen zu den Filtern", + "Panoramax sent a map tile that could not be read": "Panoramax hat eine unlesbare Kartenkachel gesendet", + "This Panoramax photo is no longer available": "Dieses Panoramax-Foto ist nicht mehr verfügbar", + "zoom in to see Panoramax coverage": "hineinzoomen, um die Panoramax-Abdeckung zu sehen", + "Panoramax did not answer in time — try again": "Panoramax hat nicht rechtzeitig geantwortet – erneut versuchen", + "Panoramax could not be reached — check the connection": "Panoramax ist nicht erreichbar – Verbindung prüfen", + "Panoramax sent too much data": "Panoramax hat zu viele Daten gesendet", + "Panoramax is busy": "Panoramax ist ausgelastet", + "Panoramax sent an unreadable answer": "Panoramax hat eine unlesbare Antwort gesendet", + "Sequence images unavailable": "Sequenzbilder nicht verfügbar", + "No street-level imagery providers configured": "Keine Anbieter für Bilder auf Straßenebene konfiguriert", + "(by upload date)": "(nach Upload-Datum)", + "Open the Street Level panel to view imagery": "Bereich Straßenebene öffnen, um Bilder anzuzeigen", + "Image could not be opened": "Bild konnte nicht geöffnet werden", + "TeleGeography load failed": "TeleGeography konnte nicht geladen werden", + "UNAVAILABLE · {source} · Roads unavailable": "NICHT VERFÜGBAR · {source} · Straßen nicht verfügbar", + "Roads: {source}": "Straßen: {source}", + "TomTom roads unavailable while the traffic service is unreachable": "TomTom-Straßen nicht verfügbar, solange der Verkehrsdienst nicht erreichbar ist", + "Hybrid unavailable while the traffic service is unreachable": "Hybrid nicht verfügbar, solange der Verkehrsdienst nicht erreichbar ist", + "TomTom roads need a TomTom key": "TomTom-Straßen brauchen einen TomTom-Schlüssel", + "Hybrid needs a TomTom key": "Hybrid braucht einen TomTom-Schlüssel", + "Local surface still loading": "Lokale Oberfläche lädt noch", + "Partial coverage": "Teilweise Abdeckung", + "Detailed roads unavailable": "Detaillierte Straßen nicht verfügbar", + "Reduced detail coverage": "Reduzierte Detailabdeckung", + "TomTom roads only, each with its live flow": "Nur TomTom-Straßen, jeweils mit Live-Verkehrsfluss", + "TomTom roads only, each with its live flow (needs a TomTom key)": "Nur TomTom-Straßen, jeweils mit Live-Verkehrsfluss (braucht einen TomTom-Schlüssel)", + "OpenStreetMap roads, with TomTom flow matched when available": "OpenStreetMap-Straßen, mit TomTom-Verkehrsfluss, wo verfügbar", + "TomTom roads first; OpenStreetMap fills the rest, simulated": "Zuerst TomTom-Straßen; OpenStreetMap füllt den Rest, simuliert", + "TomTom roads first; OpenStreetMap fills the rest, simulated (needs a TomTom key)": "Zuerst TomTom-Straßen; OpenStreetMap füllt den Rest, simuliert (braucht einen TomTom-Schlüssel)", + "Hybrid": "Hybrid", + "TomTom key unavailable": "TomTom-Schlüssel nicht verfügbar", + "TomTom daily budget reached": "TomTom-Tagesbudget erreicht", + "TomTom upstream unreachable": "TomTom-Upstream nicht erreichbar", + "TomTom flow unavailable": "TomTom-Verkehrsfluss nicht verfügbar", + "SIMULATED — {reason}": "SIMULIERT – {reason}", + "OpenFreeMap tiles rate-limited": "OpenFreeMap-Kacheln: Anfragelimit erreicht", + "OpenFreeMap tiles timed out": "Zeitüberschreitung bei OpenFreeMap-Kacheln", + "OpenFreeMap tiles unavailable": "OpenFreeMap-Kacheln nicht verfügbar", + "occupancy|empty": "leer", + "occupancy|many seats available": "viele Sitzplätze frei", + "occupancy|few seats available": "wenige Sitzplätze frei", + "occupancy|standing room only": "nur Stehplätze", + "occupancy|crushed standing room only": "nur Stehplätze, dicht gedrängt", + "occupancy|full": "voll", + "occupancy|not accepting passengers": "keine Fahrgastaufnahme", + "occupancy|not boardable": "kein Einstieg möglich", + "Route {route}": "Linie {route}", + "Vehicle {vehicle}": "Fahrzeug {vehicle}", + "vehicle motion|Stopped": "Steht", + "No recent fix": "Keine aktuelle Position", + "Waiting for update": "Warten auf Aktualisierung", + "Moving": "In Bewegung", + "reported {speed} km/h": "gemeldet {speed} km/h", + "reported hdg {heading}°": "gemeldeter Kurs {heading}°", + "Last report: stopped at stop {stop}": "Letzte Meldung: hält an Haltestelle {stop}", + "Last report: arriving at stop {stop}": "Letzte Meldung: erreicht Haltestelle {stop}", + "Last report: next stop {stop}": "Letzte Meldung: nächster Halt {stop}", + "Shown at street level · depth not in the feed": "Auf Straßenebene gezeigt · Tiefe nicht im Feed", + "Reported {seconds} s ago": "Gemeldet vor {seconds} s", + "Reported {minutes} min ago": "Gemeldet vor {minutes} min", + "shown {seconds} s behind": "{seconds} s verzögert gezeigt", + "{reported} (feed time)": "{reported} (Feed-Zeit)", + "Available trail: {minutes} min (limited)": "Verfügbare Spur: {minutes} min (begrenzt)", + "Available trail: {minutes} min": "Verfügbare Spur: {minutes} min", + "Bus": "Bus", + "Tram": "Straßenbahn", + "Subway": "U-Bahn", + "Train": "Zug", + "Ferry": "Fähre", + "Transit vehicle": "ÖPNV-Fahrzeug", + "{speed}KT": "{speed} KN", + "{heading}DEG": "{heading}°", + "AIS live load failed": "AIS-Live-Daten konnten nicht geladen werden", + "AISSTREAM_API_KEY not set": "AISSTREAM_API_KEY nicht gesetzt", + "live feed unsupported": "Live-Feed nicht unterstützt", + "connecting to feed…": "Verbindung zum Feed…", + "feed disconnected": "Feed getrennt", + "feed down": "Feed ausgefallen", + "feed idle": "Feed inaktiv", + "API key rejected — check AISSTREAM_API_KEY": "API-Schlüssel abgelehnt – AISSTREAM_API_KEY prüfen", + "feed silent {seconds}s — no AIS data": "Feed stumm seit {seconds} s – keine AIS-Daten", + "feed silent — no AIS data": "Feed stumm – keine AIS-Daten", + "feed down — retrying slowly (attempt {attempt})": "Feed ausgefallen – neue Versuche in größeren Abständen (Versuch {attempt})", + "reconnecting to feed… (attempt {attempt})": "Verbindung zum Feed wird wiederhergestellt… (Versuch {attempt})", + "feed down — retrying slowly": "Feed ausgefallen – neue Versuche in größeren Abständen", + "reconnecting to feed…": "Verbindung zum Feed wird wiederhergestellt…", + "Focus vessel {name}, MMSI {mmsi}": "Schiff {name} fokussieren, MMSI {mmsi}", + "Hidden by this map source · choose a globe map": "Von dieser Kartenquelle ausgeblendet · eine Globuskarte wählen", + "Weather image HTTP {status}": "Wetterbild HTTP {status}", + "Weather HTTP {status}": "Wetter HTTP {status}", + "Frame unavailable; previous observation retained": "Frame fehlt; letzter Stand bleibt erhalten", + "Weather imagery unavailable": "Wetterbilder nicht verfügbar", + "Weather source unavailable; previous observation retained": "Wetterquelle nicht verfügbar; vorherige Beobachtung bleibt erhalten", + "Weather unavailable": "Wetter nicht verfügbar", + "No frame within {gap} min of {time}": "Kein Frame in den {gap} min bis {time}", + "No frame within {gap} h of {time}": "Kein Frame in den {gap} h bis {time}", + "{hours}h {minutes}m ago": "vor {hours} h {minutes} min", + "{minutes}m ago": "vor {minutes} min", + "{state} · {time} · {lag}": "{state} · {time} · {lag}", + "Observed": "Beobachtet", + "History": "Verlauf", + "Observation unavailable": "Beobachtung nicht verfügbar", + "Waiting for observation": "Warten auf Beobachtung", + "{detail} · synced": "{detail} · synchronisiert", + "{detail} · nearest": "{detail} · zeitlich am nächsten", + "RADAR REFLECTIVITY · dBZ": "RADARREFLEKTIVITÄT · dBZ", + "LIGHTNING DENSITY · 15 min accumulation": "BLITZDICHTE · 15-min-Summe", + "GLOBAL INFRARED · hourly": "GLOBALES INFRAROT · stündlich", + "GOES INFRARED · ~5 min": "GOES-INFRAROT · ~5 min", + "{state}: {time}": "{state}: {time}", + "Latest observation": "Letzte Beobachtung", + "frame {index}/{total}": "Frame {index}/{total}", + "loading": "lädt", + "Observation: unavailable": "Beobachtung: nicht verfügbar", + "Contiguous US · gaps ≠ no rain": "USA ohne Alaska/Hawaii · Lücken ≠ kein Regen", + "Americas + Pacific · not individual strikes": "Amerika + Pazifik · keine Einzelblitze", + "Color: strikes/km²/min ×10³": "Farbe: Blitze/km²/min ×10³", + "60°S–60°N · typically 2–3 h delayed": "60°S–60°N · meist 2–3 h verzögert", + "North America · infrared imagery": "Nordamerika · Infrarotbilder", + "Reduced motion · history playback unavailable": "Reduzierte Bewegung · Verlaufswiedergabe nicht verfügbar", + "Reduced motion · manual history available": "Reduzierte Bewegung · manueller Verlauf verfügbar", + "Rain radar · US": "Regenradar · USA", + "Lightning density · 15 min": "Blitzdichte · 15 min", + "Satellite clouds": "Wolken (Satellit)", + "Americas + Pacific": "Amerika + Pazifik", + "Global · 60°S–60°N": "Global · 60°S–60°N", + "North America": "Nordamerika", + "Source observations delayed": "Beobachtungen der Quelle verzögert", + "Stale source": "Quelle veraltet", + "Loading next frame…": "Nächster Frame wird geladen…", + "Map center outside coverage": "Kartenmitte außerhalb der Abdeckung", + "strikes/km²/min ×10³": "Blitze/km²/min ×10³", + "N. America": "Nordamerika", + "Global": "Global", + "Hourly global mosaic; usually 2–3 hours delayed": "Stündliches globales Mosaik; meist 2–3 Stunden verzögert", + "GOES regional clouds; approximately 5-minute updates": "Regionale GOES-Wolkenbilder; Aktualisierung etwa alle 5 Minuten", + "Clouds only": "Nur Wolken", + "Apply the full cloud brightness key; 100% filter strength": "Helligkeitsfilter für Wolken vollständig anwenden; Filterstärke 100 %", + "Full": "Voll", + "Show the complete infrared image; 0% filter strength": "Vollständiges Infrarotbild zeigen; Filterstärke 0 %", + "Soft": "Weich", + "Vivid": "Kräftig", + "Image opacity; does not alter the observed values": "Bilddeckkraft; ändert die beobachteten Werte nicht", + "View US radar": "US-Radar anzeigen", + "View Americas & Pacific": "Amerika & Pazifik anzeigen", + "View coverage": "Abdeckung anzeigen", + "{value} strikes/km²/min ×10³ (15-minute density)": "{value} Blitze/km²/min ×10³ (15-Minuten-Dichte)", + "{value} dBZ radar reflectivity": "{value} dBZ Radarreflektivität", + "STALE · cached source metadata": "VERALTET · Quellmetadaten aus dem Cache", + "Map center is outside source coverage": "Kartenmitte liegt außerhalb der Quellabdeckung", + "NOAA/NWS 15-minute lightning density derived from Vaisala NLDN/GLD360. Coverage 110°E across the Pacific/Americas to 0°, 25°S–80°N. Not a live strike count, global coverage or a safety warning.": "NOAA/NWS-Blitzdichte über 15 Minuten, abgeleitet aus Vaisala NLDN/GLD360. Abdeckung von 110°O über Pazifik/Amerika bis 0°, 25°S–80°N. Keine Live-Blitzzählung, keine globale Abdeckung und keine Sicherheitswarnung.", + "NOAA MRMS radar echoes indicate precipitation patterns, not rain rate, a storm warning or a future forecast. Native source approximately 1 km; display is limited to level 6. Frames use exact advertised observation times.": "NOAA-MRMS-Radarechos zeigen Niederschlagsmuster, nicht die Regenrate, keine Unwetterwarnung und keine Vorhersage. Native Quellauflösung etwa 1 km; die Anzeige ist auf Stufe 6 begrenzt. Frames verwenden die exakt angegebenen Beobachtungszeiten.", + "GOES-19/18 longwave infrared Band 14 regional; NESDIS global longwave mosaic. Clouds only dims everything but bright, cold cloud tops; a brightness filter, not a cloud mask. Coverage and freshness differ by region.": "GOES-19/18 langwelliges Infrarot Band 14 regional; globales langwelliges NESDIS-Mosaik. „Nur Wolken“ dunkelt alles außer hellen, kalten Wolkenobergrenzen ab; ein Helligkeitsfilter, keine Wolkenmaske. Abdeckung und Aktualität unterscheiden sich je nach Region.", + "REGION": "REGION", + "IMAGE": "BILD", + "FILTER": "FILTER", + "Cloud filter strength": "Stärke des Wolkenfilters", + "OPACITY": "DECKKRAFT", + "Infrared decode failed": "Infrarot-Dekodierung fehlgeschlagen", + "Weather prefetch unavailable": "Wetter-Vorabruf nicht verfügbar", + "Weather tiles unavailable · previous frame retained": "Wetterkacheln nicht verfügbar · vorheriger Frame bleibt erhalten", + "Some weather tiles unavailable": "Einige Wetterkacheln nicht verfügbar", + "Malformed weather manifest": "Fehlerhaftes Wetter-Manifest", + "Weather request timed out": "Zeitüberschreitung bei der Wetterabfrage", + "Temperature field unavailable": "Temperaturfeld nicht verfügbar", + "Mean sea level pressure field unavailable": "Feld des Luftdrucks auf Meereshöhe nicht verfügbar", + "Wind upstream unavailable": "Wind-Upstream nicht verfügbar", + "{source} HTTP {status}": "{source} HTTP {status}", + "Wind": "Wind", + "Wind source unavailable": "Windquelle nicht verfügbar", + "Forecast · does not follow history": "Vorhersage · folgt nicht dem Verlauf", + "Wind motion": "Windbewegung", + "Wind speed": "Windgeschwindigkeit", + "Sea-level pressure": "Luftdruck auf Meereshöhe", + "Air temperature · 2 m": "Lufttemperatur · 2 m", + "Global · 1° grid": "Global · 1°-Raster", + "{model} forecast · {time}": "{model}-Vorhersage · {time}", + "Loading forecast": "Vorhersage wird geladen", + "Preparing flow": "Strömung wird vorbereitet", + "Selected field unavailable": "Gewähltes Feld nicht verfügbar", + "Cached forecast · stale": "Vorhersage aus dem Cache · veraltet", + "ECMWF IFS surface forecast": "ECMWF-IFS-Bodenvorhersage", + "NOAA GFS surface forecast": "NOAA-GFS-Bodenvorhersage", + "Reduced motion": "Reduzierte Bewegung", + "Pause visual flow; forecast time does not advance with animation": "Visuelle Strömung pausieren; die Vorhersagezeit läuft mit der Animation nicht weiter", + "None": "Ohne", + "Speed": "Geschwindigkeit", + "Pressure": "Luftdruck", + "Temperature": "Temperatur", + "Wind trails with no field shading": "Windspuren ohne Feldschattierung", + "How hard the surface wind is blowing": "Wie stark der Bodenwind weht", + "Sea-level air pressure: broad highs and lows": "Luftdruck auf Meereshöhe: großräumige Hochs und Tiefs", + "Air temperature two meters above the surface": "Lufttemperatur zwei Meter über dem Boden", + "Wind speed units": "Einheiten der Windgeschwindigkeit", + "Read wind at map center": "Wind in der Kartenmitte ablesen", + "Read the forecast at the center of the map without changing selection": "Vorhersage in der Kartenmitte ablesen, ohne die Auswahl zu ändern", + "{model} forecast · {field} ({unit})": "{model}-Vorhersage · {field} ({unit})", + "Valid: {time}": "Gültig: {time}", + "preparing": "wird vorbereitet", + "Issued: {time}": "Ausgegeben: {time}", + "Selected field unavailable · wind remains visible": "Gewähltes Feld nicht verfügbar · Wind bleibt sichtbar", + "{error} · wind remains visible": "{error} · Wind bleibt sichtbar", + "Surface wind at 10 m. Approximately 1° global grid. Curves follow the 10 m wind field, lifted 12 km for visibility; display height is not weather altitude. View lighting is for readability. Animation shows flow through one fixed forecast; it does not advance time. Color fields drape the globe basemap or the active photorealistic 3D Tiles.": "Bodenwind in 10 m Höhe. Globales Raster von etwa 1°. Die Kurven folgen dem 10-m-Windfeld, zur Sichtbarkeit 12 km angehoben; die Darstellungshöhe hat keine meteorologische Bedeutung. Die Beleuchtung dient der Lesbarkeit. Die Animation zeigt die Strömung einer festen Vorhersage; die Zeit läuft nicht weiter. Farbfelder liegen auf der Globus-Basiskarte oder den aktiven fotorealistischen 3D Tiles.", + "MODEL": "MODELL", + "FIELD": "FELD", + "UNITS": "EINHEITEN", + "MOTION": "BEWEGUNG", + "Forecast": "Vorhersage", + "N": "N", + "NNE": "NNO", + "NE": "NO", + "ENE": "ONO", + "E": "O", + "ESE": "OSO", + "SE": "SO", + "SSE": "SSO", + "S": "S", + "SSW": "SSW", + "SW": "SW", + "WSW": "WSW", + "W": "W", + "WNW": "WNW", + "NW": "NW", + "NNW": "NNW", + "No surface reading": "Kein bodennaher Wert", + "Aim the map center at Earth": "Kartenmitte auf die Erde richten", + "A location reading needs a loaded forecast and the Earth at the center of the view.": "Ein Ortswert braucht eine geladene Vorhersage und die Erde in der Bildmitte.", + "Interpolated model forecast on an approximately 1° grid. Broad weather patterns, not a street-level measurement.": "Interpolierte Modellvorhersage auf einem Raster von etwa 1°. Großräumige Wetterlagen, keine Messung auf Straßenebene.", + " · calm": " · windstill", + " from {direction}": " aus {direction}", + "WIND AT {coordinates}": "WIND BEI {coordinates}", + "{model} · valid {time}": "{model} · gültig {time}", + "Wind speed field unavailable": "Feld der Windgeschwindigkeit nicht verfügbar", + "Globe imagery unavailable": "Globusbilder nicht verfügbar", + "Globe field image unavailable": "Feldbild für den Globus nicht verfügbar", + "Unknown wind model": "Unbekanntes Windmodell", + "Unknown weather overlay": "Unbekanntes Wetter-Overlay", + "Wind request timed out": "Zeitüberschreitung bei der Windabfrage", + "Malformed wind manifest": "Fehlerhaftes Wind-Manifest", + "Malformed wind grid": "Fehlerhaftes Windraster", + "Google 3D tiles unavailable — check the key's API restrictions, quota, or network": "Google 3D Tiles nicht verfügbar – API-Einschränkungen des Schlüssels, Kontingent oder Netzwerk prüfen", + "Needs {envVars} — add it in Provider Settings — or a Cesium ion token for the ion-hosted route": "Benötigt {envVars} – in den Anbieter-Einstellungen hinzufügen – oder ein Cesium-ion-Token für die von ion gehostete Variante", + "Exit cockpit to fly to a camera": "Cockpit verlassen, um zu einer Kamera zu fliegen", + "Exit cockpit to fly to a location": "Cockpit verlassen, um zu einem Ort zu fliegen", + "Exit cockpit to fly to a photo": "Cockpit verlassen, um zu einem Foto zu fliegen", + "Exit cockpit to fly to a shared view": "Cockpit verlassen, um zu einer geteilten Ansicht zu fliegen", + "Exit cockpit to fly to a target": "Cockpit verlassen, um zu einem Ziel zu fliegen", + "Exit cockpit to fly to a vessel": "Cockpit verlassen, um zu einem Schiff zu fliegen", + "Exit cockpit to fly to a fire": "Cockpit verlassen, um zu einem Brand zu fliegen", + "Exit cockpit to fly to a route": "Cockpit verlassen, um zu einer Route zu fliegen", + "Exit cockpit to fly to a file": "Cockpit verlassen, um zu einer Datei zu fliegen", + "Exit cockpit to fly to a scene": "Cockpit verlassen, um zu einer Szene zu fliegen", + "Exit cockpit to view weather coverage": "Cockpit verlassen, um die Wetterabdeckung anzuzeigen", + "Exit cockpit to fly to a cyclone": "Cockpit verlassen, um zu einem Wirbelsturm zu fliegen", + "Exit cockpit to fly to an aircraft": "Cockpit verlassen, um zu einem Luftfahrzeug zu fliegen", + "Exit cockpit to fly to a satellite": "Cockpit verlassen, um zu einem Satelliten zu fliegen", + "Exit cockpit to frame contacts": "Cockpit verlassen, um Kontakte ins Bild zu nehmen", + "Focusing {label}": "{label} wird fokussiert", + "Source": "Quelle", + "Camera ownership changed": "Kamerasteuerung hat gewechselt", + "Saved project could not be read; storage preserved. Import a valid file to resume saving.": "Gespeichertes Projekt konnte nicht gelesen werden; der Speicher bleibt erhalten. Eine gültige Datei importieren, um wieder speichern zu können.", + "Scene layer turned off": "Szenenebene ausgeschaltet", + "Stopped": "Gestoppt", + "Scene not saved — existing saved project could not be read. Export edits or import a valid file.": "Szene nicht gespeichert – das vorhandene gespeicherte Projekt konnte nicht gelesen werden. Änderungen exportieren oder eine gültige Datei importieren.", + "Scene not saved — {reason}": "Szene nicht gespeichert – {reason}", + "Scene not saved — browser storage unavailable": "Szene nicht gespeichert – Browserspeicher nicht verfügbar", + "Cannot update {scene}: shot inventory changed": "{scene} kann nicht aktualisiert werden: Aufnahmenbestand hat sich geändert", + "Cannot update {scene}: evidence beats changed": "{scene} kann nicht aktualisiert werden: Beweisschritte haben sich geändert", + "Cannot update {scene}: shot bindings are incomplete": "{scene} kann nicht aktualisiert werden: Aufnahmeverknüpfungen sind unvollständig", + "Updated {count} shots: {scene}": { + "one": "{count} Aufnahme aktualisiert: {scene}", + "other": "{count} Aufnahmen aktualisiert: {scene}" + }, + "Appended {count} shots: {scene}": { + "one": "{count} Aufnahme angehängt: {scene}", + "other": "{count} Aufnahmen angehängt: {scene}" + }, + "Could not leave scene: {scene}": "Szene konnte nicht verlassen werden: {scene}", + "Seeked: {scene} / {shot}": "Gesprungen: {scene} / {shot}", + "Seeking scene clock": "Szenenuhr wird gespult", + "Scene transition limit reached — load a shot to reset": "Limit für Szenenwechsel erreicht – eine Aufnahme laden, um es zurückzusetzen", + "Export failed: {error}": "Export fehlgeschlagen: {error}", + "Importing project": "Projekt wird importiert", + "Imported {file}": "{file} importiert", + "Import failed: {error}": "Import fehlgeschlagen: {error}", + "Import failed (could not read JSON file)": "Import fehlgeschlagen (JSON-Datei konnte nicht gelesen werden)", + "Layers refused: {layers}": "Ebenen abgelehnt: {layers}", + "Camera move interrupted": "Kameraflug unterbrochen", + "Scene media timed out": "Zeitüberschreitung bei Szenenmedien", + "Action complete": "Aktion abgeschlossen", + "Action unavailable or cancelled": "Aktion nicht verfügbar oder abgebrochen", + "Scene actions": "Szenenaktionen", + "Select a feature or use Tab and Enter to choose an action.": "Ein Objekt auswählen oder mit Tab und Enter eine Aktion wählen.", + "Selected feature: {feature}. Choose an action.": "Ausgewähltes Objekt: {feature}. Aktion wählen.", + "Bhote Koshi Evidence Sequence": "Bhote Koshi: Beweisfolge", + "Nepal Flood Incident": "Flutereignis Nepal", + "Global Incident Context": "Globaler Ereigniskontext", + "Nepal-Focused Globe Rotation": "Globusdrehung mit Fokus auf Nepal", + "Bhote Koshi Regional Approach": "Bhote Koshi: Anflug auf die Region", + "Bhote Koshi Nearby Cities": "Bhote Koshi: Städte in der Nähe", + "Bhote Koshi Incident Corridor": "Bhote Koshi: Ereigniskorridor", + "Bhote Koshi Flood Path": "Bhote Koshi: Flutpfad", + "Bhote Koshi Corridor Overview": "Bhote Koshi: Korridor im Überblick", + "Bhote Koshi Upper Valley": "Bhote Koshi: oberes Tal", + "Upper Valley Collapse": "Erdrutsch im oberen Tal", + "Debris-Dammed Lake": "Durch Geröll aufgestauter See", + "Nepal-China Border Gate": "Grenzübergang Nepal–China", + "Timure Evidence Cluster": "Beweiscluster Timure", + "Syabru Besi Passage": "Durchgang Syabru Besi", + "Bidur / Trishuli Consequence": "Folgen in Bidur / Trishuli", + "Final view": "Schlussansicht", + "Scene {n}": "Szene {n}", + "the title of scene {scene} is empty or longer than {max} characters": "der Titel von Szene {scene} ist leer oder länger als {max} Zeichen", + "the title of shot {shot} in scene {scene} is empty or longer than {max} characters": "der Titel von Aufnahme {shot} in Szene {scene} ist leer oder länger als {max} Zeichen", + "the project is larger than 5 MiB": "das Projekt ist größer als 5 MiB", + "the project has too many shots": "das Projekt hat zu viele Aufnahmen", + "the scene data failed validation": "die Szenendaten haben die Validierung nicht bestanden", + "Could not complete this action. Check the file, references and selected assets.": "Diese Aktion konnte nicht abgeschlossen werden. Datei, Verweise und ausgewählte Assets prüfen.", + "{count} scenes": { + "one": "{count} Szene", + "other": "{count} Szenen" + }, + "{count} shots": { + "one": "{count} Aufnahme", + "other": "{count} Aufnahmen" + }, + "{count} data packs": { + "one": "{count} Datenpaket", + "other": "{count} Datenpakete" + }, + "{scene} / {pack}: {status}. File: {path}. {attribution} · {license}": "{scene} / {pack}: {status}. Datei: {path}. {attribution} · {license}", + "Unavailable layers: {layers}": "Nicht verfügbare Ebenen: {layers}", + "Uses registered content or linked media. Those external files are not included in a scene bundle. Their original notices still apply.": "Verwendet registrierte Inhalte oder verknüpfte Medien. Diese externen Dateien sind nicht im Szenen-Bundle enthalten. Ihre ursprünglichen Hinweise gelten weiterhin.", + "{bytes} bundled bytes verified. Files stay in memory for this session. Reimport the bundle after reloading the app.": "{bytes} gebündelte Bytes verifiziert. Die Dateien bleiben für diese Sitzung im Speicher. Nach dem Neuladen der App das Bundle erneut importieren.", + "Review scene import": "Szenenimport prüfen", + "Nothing is loaded or changed until you apply this file.": "Nichts wird geladen oder geändert, bis diese Datei angewendet wird.", + "Ready to import": "Bereit zum Import", + "Apply replaces the current project. Export your current project first if you want to keep both.": "Anwenden ersetzt das aktuelle Projekt. Zuerst das aktuelle Projekt exportieren, um beide zu behalten.", + "Apply import": "Import anwenden", + "Import was not applied. The current project may have changed.": "Der Import wurde nicht angewendet. Das aktuelle Projekt hat sich möglicherweise geändert.", + "Select a scene and shot first": "Zuerst eine Szene und eine Aufnahme wählen", + "Edit scene details": "Szenendetails bearbeiten", + "Camera positions use degrees and meters above the ellipsoid. Drafts are validated before they replace your saved scene.": "Kamerapositionen werden in Grad und Metern über dem Ellipsoid angegeben. Entwürfe werden geprüft, bevor sie die gespeicherte Szene ersetzen.", + "Anchors and data packs": "Anker und Datenpakete", + "Shot camera, timing, packs and actions": "Aufnahme: Kamera, Zeitablauf, Datenpakete und Aktionen", + "New anchor ID": "Neue Anker-ID", + "Capture camera as anchor": "Kamera als Anker erfassen", + "Anchor added to draft": "Anker zum Entwurf hinzugefügt", + "Set move start to current camera": "Aktuelle Kameraposition als Startpunkt des Flugs übernehmen", + "Move added to draft; destination remains the shot camera": "Flug zum Entwurf hinzugefügt; das Ziel bleibt die Kameraposition der Aufnahme", + "Use ordinary flight": "Normalen Flug verwenden", + "Invalid shot JSON": "Ungültiges Aufnahme-JSON", + "Apply details": "Details anwenden", + "Select a scene first": "Zuerst eine Szene wählen", + "Share selected scene": "Gewählte Szene teilen", + "Scene JSON preserves authored settings and attribution. It does not include asset files. Bundles include only pack files you choose here or previously imported bundle files.": "Szenen-JSON bewahrt die festgelegten Einstellungen und Quellenangaben. Asset-Dateien sind nicht enthalten. Bundles enthalten nur die hier gewählten Paketdateien oder zuvor importierte Bundle-Dateien.", + "Download scene JSON": "Szenen-JSON herunterladen", + "Choose data-pack files": "Datenpaket-Dateien wählen", + "Or choose a data-pack folder": "Oder einen Datenpaket-Ordner wählen", + "Download asset bundle": "Asset-Bundle herunterladen", + "Asset bundle downloaded": "Asset-Bundle heruntergeladen", + "No RTL-SDR device selected": "Kein RTL-SDR-Gerät ausgewählt", + "WebUSB requires desktop Chrome or Edge on localhost/HTTPS": "WebUSB braucht Chrome oder Edge auf dem Desktop über localhost/HTTPS", + "RTL-SDR access failed; close other SDR apps and check USB permissions": "RTL-SDR-Zugriff fehlgeschlagen; andere SDR-Apps schließen und USB-Berechtigungen prüfen", + "RTL-SDR connection failed": "RTL-SDR-Verbindung fehlgeschlagen", + "Receiving 1090 MHz ADS-B": "Empfang von ADS-B auf 1090 MHz", + "Receiving broadcast FM": "Empfang von UKW-Rundfunk", + "Connect an RTL-SDR to begin.": "Ein RTL-SDR anschließen, um zu beginnen.", + "WebUSB is unavailable in this browser": "WebUSB ist in diesem Browser nicht verfügbar", + "USB work abandoned after a stalled transfer": "USB-Vorgang nach einer hängenden Übertragung abgebrochen", + "SDR decoder worker failed": "SDR-Decoder-Worker fehlgeschlagen", + "SDR decoder did not start; receiver stopped": "SDR-Decoder ist nicht gestartet; Empfänger gestoppt", + "SDR decoder stopped responding; receiver stopped": "SDR-Decoder reagiert nicht mehr; Empfänger gestoppt", + "SDR decoder fell behind; receiver stopped": "SDR-Decoder kommt nicht hinterher; Empfänger gestoppt", + "WebUSB requires desktop Chrome or Edge": "WebUSB braucht Chrome oder Edge auf dem Desktop", + "Waiting for an RTL-SDR device…": "Warten auf ein RTL-SDR-Gerät…", + "Browser audio is suspended; press Connect again to resume it": "Browser-Audio ist angehalten; erneut auf Verbinden drücken, um es fortzusetzen", + "RTL-SDR sample stream stopped": "RTL-SDR-Sample-Stream gestoppt", + "Switching to {mode}…": "Wechsel zu {mode}…", + "Browser audio is suspended; select FM again to resume it": "Browser-Audio ist angehalten; erneut FM wählen, um es fortzusetzen", + "RTL-SDR gain change failed": "Verstärkung des RTL-SDR konnte nicht geändert werden", + "Tuned to {mhz} MHz FM": "Abgestimmt auf {mhz} MHz UKW", + "Seeking up…": "Suchlauf aufwärts…", + "Seeking down…": "Suchlauf abwärts…", + "Seek stopped": "Suchlauf gestoppt", + "Scanning {mhz} MHz…": "{mhz} MHz wird gescannt…", + "Found {mhz} MHz · {snr} dB": "{mhz} MHz gefunden · {snr} dB", + "FM signal found at {mhz} MHz": "UKW-Signal auf {mhz} MHz gefunden", + "No FM signal found": "Kein UKW-Signal gefunden", + "Browser location is unavailable": "Browser-Standort ist nicht verfügbar", + "Receiver location was not granted": "Standort des Empfängers wurde nicht freigegeben", + "RTL-SDR disconnected": "RTL-SDR getrennt", + "Upstream response too large": "Upstream-Antwort zu groß", + "Vector tile request timed out": "Zeitüberschreitung bei der Vektorkachel-Anfrage", + "Vector tiles unavailable": "Vektorkacheln nicht verfügbar", + "Vector tile metadata unavailable": "Metadaten der Vektorkacheln nicht verfügbar", + "Zoom in for vector tile coverage": "Hineinzoomen für die Abdeckung mit Vektorkacheln", + "Saturation": "Sättigung", + "Edge Thickness": "Kantenstärke", + "Contrast": "Kontrast", + "Grain": "Körnung", + "Vignette": "Vignette", + "Pixelation": "Verpixelung", + "Distortion": "Verzerrung", + "Instability": "Instabilität", + "Gain": "Verstärkung", + "Scanlines": "Scanlines", + "Sensitivity": "Empfindlichkeit", + "WHOT/BHOT": "WHOT/BHOT", + "Ironbow": "Ironbow", + "unsaved CCTV calibration": "ungespeicherte CCTV-Kalibrierung", + "the open cockpit": "das geöffnete Cockpit", + "Switching language reloads the page. This will be lost: {items}. Switch anyway?": "Ein Sprachwechsel lädt die Seite neu. Dabei geht Folgendes verloren: {items}. Trotzdem wechseln?", + "Link shared": "Link geteilt", + "Return map to straight-down view": "Karte wieder senkrecht von oben zeigen", + "Tilt map to oblique view": "Karte in Schrägansicht kippen", + "Reset map to north up. Current heading {heading} degrees": "Karte nach Norden ausrichten. Aktuelle Ausrichtung {heading} Grad", + "Tilted view": "Schrägansicht", + "Straight-down view": "Senkrechte Ansicht", + "North up": "Norden oben", + "HDG": "HDG", + "PITCH": "NEIGUNG", + "FOV": "FOV", + "RANGE": "REICHW.", + "HGT": "HÖHE", + "ΔN": "ΔN", + "ΔE": "ΔO", + "{value}m": "{value} m", + "SNAPSHOT": "SNAPSHOT", + "LIVE": "LIVE", + "UPSTREAM": "UPSTREAM", + "SYNTHETIC": "SYNTHETISCH", + "CONFIGURED": "KONFIGURIERT", + "SEED": "KATALOG", + "OK": "OK", + "Live HLS connected": "Live-HLS verbunden", + "Live HLS unavailable": "Live-HLS nicht verfügbar", + "No stream URL configured": "Keine Stream-URL konfiguriert", + "Live stream connected": "Live-Stream verbunden", + "Snapshot feed connected": "Snapshot-Feed verbunden", + "Media fetch failed": "Medienabruf fehlgeschlagen", + "Upstream snapshot active": "Upstream-Snapshot aktiv", + "Fallback Street View frame": "Fallback-Bild aus Street View", + "Upstream unavailable": "Upstream nicht verfügbar", + "No source configured": "Keine Quelle konfiguriert", + "Configured source": "Konfigurierte Quelle", + "Configured Source": "Konfigurierte Quelle", + "Synthetic fallback": "Synthetischer Fallback", + "Upstream HTTP {status}": "Upstream HTTP {status}", + "Unexpected media type {type}": "Unerwarteter Medientyp {type}", + "{value} KM/H": "{value} KM/H", + "{label} · {radius} AIR/SEA WINDOW": "{label} · {radius} LUFT-/SEEFENSTER", + "{scope} · INSTALLATIONS {coverage}": "{scope} · ANLAGEN {coverage}", + "SQUAWK {code} · {status}": "SQUAWK {code} · {status}", + "ADS-B PRIORITY · {status}": "ADS-B-PRIORITÄT · {status}", + "ADS-B EMERGENCY · {status}": "ADS-B-NOTFALL · {status}", + "{aircraftClass} · {emergency} · {feedState}": "{aircraftClass} · {emergency} · {feedState}", + "{source} · LOCATION QUERY · RECENT": "{source} · ORTSABFRAGE · AKTUELL", + "Rendered globe frames per second · toggle with `": "Gerenderte Globus-Frames pro Sekunde · umschalten mit `", + "imported map files": "importierte Kartendateien", + "Ground heights could not be refreshed. Try the file again when the map has loaded.": "Geländehöhen konnten nicht aktualisiert werden. Die Datei erneut laden, wenn die Karte geladen ist.", + "{count} map objects": { + "one": "{count} Kartenobjekt", + "other": "{count} Kartenobjekte" + }, + ", drawn with its own styles": ", mit eigenen Stilen gezeichnet", + "Hide {name}": "{name} ausblenden", + "Show {name}": "{name} einblenden", + "Fly to {name}": "Zu {name} fliegen", + "Remove {name}": "{name} entfernen", + "At most {count} files at once — remove one first.": "Höchstens {count} Dateien gleichzeitig – zuerst eine entfernen.", + "Reading {name}…": "{name} wird gelesen…", + "That file could not be read.": "Diese Datei konnte nicht gelesen werden.", + "Added {count} files.": { + "one": "{count} Datei hinzugefügt.", + "other": "{count} Dateien hinzugefügt." + }, + "{count} files could not be added ({name}: {error})": { + "one": "{count} Datei konnte nicht hinzugefügt werden ({name}: {error})", + "other": "{count} Dateien konnten nicht hinzugefügt werden ({name}: {error})" + }, + "Some KML content was left out.": "Einige KML-Inhalte wurden ausgelassen.", + "Press on the ground, not the sky": "Auf den Boden klicken, nicht in den Himmel", + "Drag to size the box": "Ziehen, um die Größe des Bereichs festzulegen", + "light {source} to use your {name} key": "{source} einschalten, um den eigenen {name}-Schlüssel zu nutzen", + "light {source}": "{source} einschalten", + ", or ": " oder ", + "{requirement}, or {offer}": "{requirement} oder {offer}", + "Local ADS-B": "Lokales ADS-B", + "Transit": "ÖPNV", + "Recent Imagery": "Aktuelle Bilddaten", + "Fire Perimeters": "Brandgrenzen", + "GNSS Integrity": "GNSS-Integrität", + "S2 GhostMaps": "S2 GhostMaps", + "Rain radar": "Regenradar", + "Lightning density": "Blitzdichte", + "Cyclone advisories": "Wirbelstürme", + "ALPR Cameras": "ALPR-Kameras", + "Live Vessels": "Live-Schiffe", + "Bike Share": "Bikesharing", + "Cameras": "Kameras", + "Street Level": "Straßenebene", + "Mapped ALPR Cameras": "Kartierte ALPR-Kameras", + "Data Centers": "Rechenzentren", + "Active Fires": "Aktive Brände", + "Military aircraft": "Militärflugzeuge", + "Vessels": "Schiffe", + "CCTV + Street View fallback": "CCTV + Street-View-Fallback", + "OpenStreetMap + optional Google Maps Places": "OpenStreetMap + optional Google Maps Places", + "GFS / ECMWF IFS · FORECAST": "GFS / ECMWF IFS · VORHERSAGE", + "NOAA nowCOAST · OBSERVED": "NOAA nowCOAST · BEOBACHTET", + "NASA FIRMS · LIVE": "NASA FIRMS · LIVE", + "OSM routing": "OSM-Routing", + "data source|Local": "Lokal", + "Movement": "Bewegung", + "Infrastructure": "Infrastruktur", + "Events": "Ereignisse", + "Weather": "Wetter", + "Utilities": "Werkzeuge", + "Other layers": "Weitere Ebenen", + "status chip|UNAVAILABLE": "NICHT VERFÜGBAR", + "Data source unavailable": "Datenquelle nicht verfügbar", + "Local FM stopped: internet radio started": "Lokales UKW gestoppt: Internetradio gestartet", + "IDLE": "INAKTIV", + "TUNING": "STIMMT AB", + "STREAMING": "STREAMT", + "UNSUPPORTED": "NICHT UNTERSTÜTZT", + "idle": "inaktiv", + "running": "läuft", + "suspended": "angehalten", + "closed": "geschlossen", + "feed status|live": "live", + "feed status|stale": "veraltet", + "feed status|unreachable": "nicht erreichbar", + "feed status|invalid": "ungültig", + "waiting for IQ": "warten auf IQ", + "{count} messages": { + "one": "{count} Nachricht", + "other": "{count} Nachrichten" + }, + "DSP {count} blocks": { + "one": "DSP {count} Block", + "other": "DSP {count} Blöcke" + }, + "DSP waiting": "DSP wartet", + "{level} dBFS audio": "Audio {level} dBFS", + "audio signal --": "Audiosignal --", + "audio {state}": "Audio {state}", + "{count} HEARD": { + "one": "{count} EMPFANGEN", + "other": "{count} EMPFANGEN" + }, + "DISCONNECT": "TRENNEN", + "CONNECT": "VERBINDEN", + "LOCATING…": "ORTUNG…", + "LOCATED": "GEORTET", + "LOCATE": "ORTEN", + " · read while Local ADS-B is on": " · gelesen, solange „Lokales ADS-B“ an ist", + "Decoder feeds: {feeds}": "Decoder-Feeds: {feeds}", + "Language: {language}": "Sprache: {language}", + "Change the interface language": "Sprache der Oberfläche ändern", + "Interface language": "Sprache der Oberfläche", + "Search languages": "Sprachen durchsuchen", + "No matching languages": "Keine passenden Sprachen", + "Check the spelling and press Enter to search again.": "Schreibweise prüfen und Enter drücken, um erneut zu suchen.", + "Press Enter to try again.": "Enter drücken, um es erneut zu versuchen.", + "Detailed outline unavailable": "Detaillierter Umriss nicht verfügbar", + "Layers": "Ebenen", + "Search": "Suche", + "More": "Mehr", + "Panels": "Bereiche", + "Austin": "Austin", + "San Francisco": "San Francisco", + "New York": "New York", + "Tokyo": "Tokio", + "London": "London", + "Paris": "Paris", + "Dubai": "Dubai", + "Washington DC": "Washington DC", + "Tallinn": "Tallinn", + "Los Angeles": "Los Angeles", + "CH {index} / {total}": "KAN {index} / {total}", + "Station unavailable after directory refresh": "Sender nach Verzeichnisaktualisierung nicht verfügbar", + "{category} ({count})": "{category} ({count})", + "PAUSE": "PAUSE", + "RESUME": "FORTSETZEN", + "Clear reading": "Wert ausblenden", + "Earlier observation": "Frühere Beobachtung", + "Observed history": "Beobachteter Verlauf", + "Later observation": "Spätere Beobachtung", + "Latest": "Neueste", + "Newest frame per product": "Neuester Frame je Produkt", + "Replay observed history": "Beobachteten Verlauf abspielen", + "Imagery on Esri · Google 3D returns when cleared": "Bilder auf Esri · nach LÖSCHEN wieder Google 3D", + "Point the camera at the ground to use the view": "Kamera auf den Boden richten, um die Ansicht zu verwenden", + "That point cannot anchor a box": "Dieser Punkt kann keinen Bereich verankern", + "Imagery stops at ±{limit}° latitude": "Bilder enden bei ±{limit}° Breite", + "{owner} is using the pointer — close it first": "{owner} verwendet gerade den Zeiger – zuerst schließen", + "Box is {size} wide · limit {limit}": "Bereich ist {size} breit · Limit {limit}", + "View is {size} wide · limit {limit}": "Ansicht ist {size} breit · Limit {limit}", + "VS BASEMAP": "VGL. KARTE", + "A / B": "A / B", + "Newest low-cloud day for this box · scene cloud, not box cloud": "Neuester Tag mit wenig Wolken für diesen Bereich · Bewölkung der Szene, nicht des Bereichs", + "Newest day covering this box · cloudier than 20%": "Neuester Tag mit Abdeckung dieses Bereichs · über 20 % Bewölkung", + "Newest day with imagery for this box · partial coverage": "Neuester Tag mit Bildern für diesen Bereich · teilweise Abdeckung", + "Newest daily overview for this box": "Neueste tägliche Übersicht für diesen Bereich", + "newest clear day": "neuester wolkenfreier Tag", + "newest day (cloudy)": "neuester Tag (bewölkt)", + "newest day (partial coverage)": "neuester Tag (teilweise Abdeckung)", + "newest overview": "neueste Übersicht", + "No imagery": "Keine Bilder", + "Loading": "Lädt", + "coverage|full": "vollständig", + "coverage|partial": "teilweise", + "unknown": "unbekannt", + "no imagery": "keine Bilder", + "{percent}% cloud": "{percent} % Bewölkung", + "{min}–{max}% cloud": "{min}–{max} % Bewölkung", + "NO BOX": "KEIN BEREICH", + "SEARCHING": "SUCHE LÄUFT", + "{count} DAYS": { + "one": "{count} TAG", + "other": "{count} TAGE" + }, + "Zoom in or draw a smaller box": "Hineinzoomen oder einen kleineren Bereich zeichnen", + "Select a box or use the view": "Bereich wählen oder die Ansicht verwenden", + "Searching the last 30 days": "Die letzten 30 Tage werden durchsucht", + "No days to show for this box": "Keine Tage für diesen Bereich", + "Drag the divider · SWAP trades sides": "Trennlinie ziehen · TAUSCHEN wechselt die Seiten", + "← → preview · A or B pins the focused day": "← → Vorschau · A oder B fixiert den gewählten Tag", + "← → preview the other side · A or B pins it": "← → Vorschau der anderen Seite · A oder B fixiert sie", + "S on another day replaces it · × unpins": "S auf einem anderen Tag ersetzt ihn · × löst die Fixierung", + "← → preview · S shows the focused day": "← → Vorschau · S zeigt den gewählten Tag", + "ZOOM IN": "HINEINZOOMEN", + "Fly in until the view fits the 1,000 km limit": "Hineinfliegen, bis die Ansicht das Limit von 1.000 km einhält", + "Imagery days, newest first · arrows move, S or A and B pin": "Bildtage, neueste zuerst · Pfeile bewegen, S oder A und B fixieren", + "On the map": "Auf der Karte", + "Mode": "Modus", + "Imagery opacity": "Bilddeckkraft", + "SWAP": "TAUSCHEN", + "Trade the two sides of the divider": "Die beiden Seiten der Trennlinie tauschen", + "SELECT BOX": "BEREICH WÄHLEN", + "Drag a box on the map (Esc cancels)": "Auf der Karte einen Bereich aufziehen (Esc bricht ab)", + "USE VIEW": "ANSICHT NUTZEN", + "Use the current view as the box": "Die aktuelle Ansicht als Bereich verwenden", + "Forget the box and its images": "Bereich und Bilder verwerfen", + "START HERE": "HIER STARTEN", + "Checking": "Wird geprüft", + "PREVIEW": "VORSCHAU", + "Newest day with imagery for this box": "Neuester Tag mit Bildern für diesen Bereich", + "SHOW": "ZEIGEN", + "Pin {day} as {slot}": "{day} als {slot} fixieren", + "Show {day}": "{day} zeigen", + "pinned {slot}": "fixiert {slot}", + "shown": "gezeigt", + "preview": "Vorschau", + "start here": "hier starten", + "No box": "Kein Bereich", + "Searching": "Suche läuft", + "Every day is empty here": "Keine Bilddaten an den geprüften Tagen", + "Sources off": "Quellen aus", + "VS": "VGL.", + "Not set": "Nicht gesetzt", + "Basemap": "Basiskarte", + "Source off": "Quelle aus", + "Unpin {slot}": "{slot} lösen", + "Unpin the image": "Bild lösen", + "EXPORT {slot}": "EXPORT {slot}", + "EXPORT": "EXPORT", + "Download {slot} as a PNG": "{slot} als PNG herunterladen", + "Download the image as a PNG": "Bild als PNG herunterladen", + "Box {width} × {height} km": "Bereich {width} × {height} km", + "Acquired {time}": "Aufgenommen {time}", + "Coverage {coverage}": "Abdeckung {coverage}", + "START HERE · {reason}": "HIER STARTEN · {reason}", + "Daily overview shows little detail in a box this small": "Die tägliche Übersicht zeigt in einem so kleinen Bereich kaum Details", + "Imagery: NASA GIBS and Worldview · HLS (Sentinel-2, Landsat 8/9) and VIIRS": "Bilder: NASA GIBS und Worldview · HLS (Sentinel-2, Landsat 8/9) und VIIRS", + "Details": "Details", + "Times, coverage, sources": "Zeiten, Abdeckung, Quellen", + "HIDE EMPTY DAYS": "LEERE TAGE AUSBLENDEN", + "SHOW EMPTY DAYS · {count}": "LEERE TAGE ZEIGEN · {count}", + "Days the probe found empty in this box": "Tage, für die die Abfrage in diesem Bereich keine Bilddaten fand", + "image": "Bild", + "BASEMAP": "BASISKARTE", + "The basemap": "Die Basiskarte", + "basemap": "Basiskarte", + "Recent imagery against the basemap divider": "Trennlinie: aktuelle Bilddaten gegen Basiskarte", + "Recent imagery A and B divider": "Trennlinie: aktuelle Bilddaten A und B", + "{before} {left} percent, {after} {right} percent": "{before} {left} Prozent, {after} {right} Prozent", + "Export failed · {error}": "Export fehlgeschlagen · {error}", + "New scene name": "Name der neuen Szene", + "Delete scene \"{scene}\" and all shots?": "Szene „{scene}“ und alle Aufnahmen löschen?", + "Stopped (Esc)": "Gestoppt (Esc)", + "No shots yet. Use CAPTURE SHOT to save current look.": "Noch keine Aufnahmen. Mit AUFNAHME SPEICHERN die aktuelle Ansicht sichern.", + "Double-click to rename": "Doppelklick zum Umbenennen", + "Shot name": "Name der Aufnahme", + "LOAD": "LADEN", + "{duration}s + {hold}s": "{duration} s + {hold} s", + "Choose files": "Dateien wählen", + "{count} files selected": { + "one": "{count} Datei ausgewählt", + "other": "{count} Dateien ausgewählt" + }, + "No file chosen": "Keine Datei gewählt", + "Cancel": "Abbrechen", + "EDIT DETAILS": "DETAILS BEARBEITEN", + "SHARE SCENE": "SZENE TEILEN", + "Shared flight follow expired": "Verfolgung des geteilten Flugs abgelaufen", + "Shared military flight follow expired": "Verfolgung des geteilten Militärflugs abgelaufen", + "Shared satellite follow expired": "Verfolgung des geteilten Satelliten abgelaufen", + "Shared flight could not be restored — feed unavailable": "Geteilter Flug konnte nicht wiederhergestellt werden – Feed nicht verfügbar", + "Shared military flight could not be restored — feed unavailable": "Geteilter Militärflug konnte nicht wiederhergestellt werden – Feed nicht verfügbar", + "Shared satellite could not be restored — feed unavailable": "Geteilter Satellit konnte nicht wiederhergestellt werden – Feed nicht verfügbar", + "Shared flight is unavailable": "Geteilter Flug ist nicht verfügbar", + "Shared military flight is unavailable": "Geteilter Militärflug ist nicht verfügbar", + "Shared satellite is unavailable": "Geteilter Satellit ist nicht verfügbar", + "{id} isn't reporting now — showing where it was last seen": "{id} sendet gerade nicht – letzte bekannte Position wird gezeigt", + "{id} isn't reporting now — showing where it was at {time}": "{id} sendet gerade nicht – Position um {time} wird gezeigt", + "This share link can't be opened": "Dieser Freigabelink kann nicht geöffnet werden", + "Shared vessel is unavailable": "Geteiltes Schiff ist nicht verfügbar", + "subject": "Objekt", + "RETRYING ALPR CAMERAS": "ALPR-KAMERAS: NEUER VERSUCH", + "FETCHING ALPR CAMERAS": "ALPR-KAMERAS WERDEN ABGERUFEN", + "OVERPASS TEMPORARILY UNAVAILABLE": "OVERPASS VORÜBERGEHEND NICHT VERFÜGBAR", + "OVERPASS RATE-LIMITED": "OVERPASS-ANFRAGELIMIT ERREICHT", + "OVERPASS TIMED OUT": "ZEITÜBERSCHREITUNG BEI OVERPASS", + "OVERPASS COULD NOT COMPLETE THE QUERY": "OVERPASS KONNTE DIE ABFRAGE NICHT ABSCHLIESSEN", + "MAP TILES TEMPORARILY UNAVAILABLE": "KARTENKACHELN VORÜBERGEHEND NICHT VERFÜGBAR", + "MAPPED NAMES TEMPORARILY UNAVAILABLE": "KARTIERTE NAMEN VORÜBERGEHEND NICHT VERFÜGBAR", + "ALPR cameras": "ALPR-Kameras", + "OpenSky OAuth client missing/invalid": "OpenSky-OAuth-Client fehlt oder ist ungültig", + "OpenSky OAuth rejected credentials": "OpenSky-OAuth hat die Zugangsdaten abgelehnt", + "OpenSky username/password rejected": "OpenSky hat Benutzername/Passwort abgelehnt", + "OpenSky auth missing": "OpenSky-Authentifizierung fehlt", + "OpenSky auth required": "OpenSky-Authentifizierung erforderlich", + "OpenSky auth invalid": "OpenSky-Authentifizierung ungültig", + "OpenSky auth failed": "OpenSky-Authentifizierung fehlgeschlagen", + "KartaView answered HTTP {status}": "KartaView hat mit HTTP {status} geantwortet", + "Panoramax answered HTTP {status}": "Panoramax hat mit HTTP {status} geantwortet", + "KartaView refused the request (code {code})": "KartaView hat die Anfrage abgelehnt (Code {code})", + "Vector tiles unavailable (HTTP {status})": "Vektorkacheln nicht verfügbar (HTTP {status})", + "GNSS feed unavailable (HTTP {status})": "GNSS-Feed nicht verfügbar (HTTP {status})", + "{count} CelesTrak groups unavailable": { + "one": "{count} CelesTrak-Gruppe nicht verfügbar", + "other": "{count} CelesTrak-Gruppen nicht verfügbar" + }, + "{feed} feed unavailable": "Feed {feed} nicht verfügbar", + "Source snapshot {minutes} min old": "Quell-Snapshot ist {minutes} min alt", + "{source} rate limited (anonymous)": "{source}: Anfragelimit erreicht (anonym)", + "{source} rate limited": "{source}: Anfragelimit erreicht", + "{source} network error": "{source}: Netzwerkfehler", + "Malformed {source} aircraft rows": "Fehlerhafte Flugzeugzeilen von {source}", + "Malformed {source} response": "Fehlerhafte Antwort von {source}", + "entry {n}": "Eintrag {n}", + "feeds {names} stale": { + "one": "Feed {names} veraltet", + "other": "Feeds {names} veraltet" + }, + "feeds {names} unreachable": { + "one": "Feed {names} nicht erreichbar", + "other": "Feeds {names} nicht erreichbar" + }, + "feeds {names} invalid": { + "one": "Feed {names} ungültig", + "other": "Feeds {names} ungültig" + }, + "Loading {feeds}": "Wird geladen: {feeds}", + "{count} heard": { + "one": "{count} empfangen", + "other": "{count} empfangen" + }, + "USB {rate} msg/s": "USB {rate} Nachr./s", + "{rate} msg/s": "{rate} Nachr./s", + "{count} feeds live": { + "one": "{count} Feed live", + "other": "{count} Feeds live" + }, + "{feed} {count} ({seconds}s old)": "{feed} {count} (vor {seconds} s)", + "Street Level toggle failed": "Umschalten der Straßenebene fehlgeschlagen", + "Street Level could not start": "Straßenebene konnte nicht gestartet werden", + "SHRINK": "VERKLEINERN", + "EXPAND": "VERGRÖSSERN", + "Shrink": "Verkleinern", + "Expand": "Vergrößern", + "ANY DATE": "BELIEBIGES DATUM", + "LAST 10 YEARS": "LETZTE 10 JAHRE", + "LAST 5 YEARS": "LETZTE 5 JAHRE", + "LAST 3 YEARS": "LETZTE 3 JAHRE", + "LAST 2 YEARS": "LETZTE 2 JAHRE", + "LAST YEAR": "LETZTE 12 MONATE", + "LAST 6 MONTHS": "LETZTE 6 MONATE", + "LAST 3 MONTHS": "LETZTE 3 MONATE", + "LAST MONTH": "LETZTE 30 TAGE", + "LAST {count} DAYS": { + "one": "{count} TAG", + "other": "LETZTE {count} TAGE" + }, + "{window} · SINCE {date}": "{window} · SEIT {date}", + "Turn Street Level off": "Straßenebene ausschalten", + "Turn Street Level on": "Straßenebene einschalten", + "{name} imagery on": "{name}-Bilder an", + "{name} imagery off": "{name}-Bilder aus", + "Image by {creator}": "Bild von {creator}", + " or ": " oder ", + "OPEN {labels}": "{labels} ÖFFNEN", + "Open {names} at the centre of the view": "{names} in der Bildmitte öffnen", + "Zoom in to a street to open {names} here": "Auf eine Straße hineinzoomen, um hier {names} zu öffnen", + "Switch Street Level on to draw its coverage.": "Straßenebene einschalten, um ihre Abdeckung zu zeichnen.", + "Every provider is switched off: light a chip to draw its coverage.": "Alle Anbieter sind ausgeschaltet: einen Chip einschalten, um seine Abdeckung zu zeichnen.", + "Loading this sequence…": "Diese Sequenz wird geladen…", + "{n} images in this sequence · Esc clears": { + "one": "{n} Bild in dieser Sequenz · Esc hebt die Auswahl auf", + "other": "{n} Bilder in dieser Sequenz · Esc hebt die Auswahl auf" + }, + "No sequences in view match the filter": "Keine Sequenzen im Bild passen zum Filter", + "No coverage in view": "Keine Abdeckung im Bild", + "{n} streets in view · click a line for its nearest photo": { + "one": "{n} Straße im Bild · Linie anklicken für das nächstgelegene Foto", + "other": "{n} Straßen im Bild · Linie anklicken für das nächstgelegene Foto" + }, + "{n} sequences in view · click a line for its nearest photo": { + "one": "{n} Sequenz im Bild · Linie anklicken für das nächstgelegene Foto", + "other": "{n} Sequenzen im Bild · Linie anklicken für das nächstgelegene Foto" + }, + "{n} sequences in view · click a line for its photos": { + "one": "{n} Sequenz im Bild · Linie anklicken für ihre Fotos", + "other": "{n} Sequenzen im Bild · Linie anklicken für ihre Fotos" + }, + "km/h": "km/h", + "m/s": "m/s", + "mph": "mph", + "hPa": "hPa", + "kt": "kn", + "kts": "kn", + "ft": "ft", + "km": "km", + "m": "m", + "h": "h", + "min": "min", + "s": "s", + "dB": "dB", + "{time} · {minutes} min ago": "{time} · vor {minutes} min", + "Active weather": "Aktives Wetter", + "LATEST · newest per product": "NEUESTE · je Produkt", + "Forecast · valid {valid} · issued {issued}": "Vorhersage · gültig {valid} · ausgegeben {issued}", + "{detail} · Does not follow history": "{detail} · Folgt nicht dem Verlauf", + "Forecast · valid {valid}": "Vorhersage · gültig {valid}", + "voice|OFF": "AUS", + "voice|CONNECTING": "VERBINDET", + "voice|ERROR": "FEHLER", + "Voice provider": "Anbieter der Sprachsteuerung", + "Changing provider reconnects voice if it is on.": "Ein Anbieterwechsel verbindet die Sprachsteuerung neu, wenn sie an ist.", + "Voice Settings": "Einstellungen der Sprachsteuerung", + "STD": "STD", + "ON/OFF": "EIN/AUS", + "VOICE": "SPRACHE", + "Preview": "Vorschau", + "Settings": "Einstellungen", + "Close voice panel": "Sprachbereich schließen", + "Voice replies appear here.": "Sprachantworten erscheinen hier.", + "Voice assistant": "Sprachassistent", + "Dismiss voice card": "Sprachkarte schließen", + "YOU": "EINGABE", + "THIS": "DIES", + "Plan": "Plan", + "Referenced items": "Erwähnte Objekte", + "NOTES & SOURCES": "NOTIZEN & QUELLEN", + "Show Voice Preview": "Sprachvorschau anzeigen", + "Voice inactivity timeout": "Zeitlimit bei Sprachinaktivität", + "Minutes": "Minuten", + "Custom": "Benutzerdefiniert", + "These preferences stay in this browser.": "Diese Einstellungen bleiben in diesem Browser.", + "Gemini returned an oversized audio chunk": "Gemini hat einen zu großen Audioblock geliefert", + "Gemini returned an unsupported audio format": "Gemini hat ein nicht unterstütztes Audioformat geliefert", + "Gemini returned an invalid audio sample rate": "Gemini hat eine ungültige Audio-Abtastrate geliefert", + "Gemini returned incomplete PCM audio": "Gemini hat unvollständiges PCM-Audio geliefert", + "This browser does not support Gemini microphone audio": "Dieser Browser unterstützt kein Mikrofon-Audio für Gemini", + "Microphone audio processing failed": "Verarbeitung des Mikrofon-Audios fehlgeschlagen", + "Gemini audio playback fell behind. Reconnect to continue.": "Die Gemini-Audiowiedergabe kommt nicht hinterher. Neu verbinden, um fortzufahren.", + "Gemini token response did not include an ephemeral credential": "Die Gemini-Token-Antwort enthielt keine temporären Zugangsdaten", + "Invalid Gemini connection endpoint": "Ungültiger Gemini-Verbindungsendpunkt", + "Gemini voice could not connect. Try again.": "Gemini-Sprachsteuerung konnte keine Verbindung herstellen. Erneut versuchen.", + "Gemini voice requires a same-origin app request": "Gemini-Sprachsteuerung erfordert eine Anfrage vom selben Origin", + "Add a Gemini API key in Provider Settings": "Einen Gemini-API-Schlüssel in den Anbieter-Einstellungen hinzufügen", + "The Gemini Live model configuration is invalid": "Die Modellkonfiguration für Gemini Live ist ungültig", + "Gemini voice is starting too often. Try again shortly.": "Gemini-Sprachsteuerung wird zu oft gestartet. Gleich erneut versuchen.", + "Gemini quota is unavailable. Try again later.": "Gemini-Kontingent ist nicht verfügbar. Später erneut versuchen.", + "Gemini access was refused. Check the API key and model access.": "Gemini hat den Zugriff verweigert. API-Schlüssel und Modellzugriff prüfen.", + "Gemini Live is unavailable for the configured model.": "Gemini Live ist für das konfigurierte Modell nicht verfügbar.", + "Token request is too large": "Token-Anfrage ist zu groß", + "Use an empty JSON token request": "Eine leere JSON-Token-Anfrage verwenden", + "Voice command context is too large": "Kontext des Sprachbefehls ist zu groß", + "Gemini reliable transport exceeded its queue limit. Start voice again.": "Die Gemini-Sendewarteschlange ist voll. Sprachsteuerung neu starten.", + "Gemini reliable transport did not drain. Start voice again.": "Gemini-Nachrichten konnten nicht rechtzeitig gesendet werden. Sprachsteuerung neu starten.", + "Gemini token request failed (HTTP {status})": "Gemini-Token-Anfrage fehlgeschlagen (HTTP {status})", + "Gemini token response did not include a valid model": "Die Gemini-Token-Antwort enthielt kein gültiges Modell", + "Gemini token response did not include session settings": "Die Gemini-Token-Antwort enthielt keine Sitzungseinstellungen", + "Gemini session settings do not match the input mode": "Die Gemini-Sitzungseinstellungen passen nicht zum Eingabemodus", + "Gemini could not submit this input. Start voice again.": "Gemini konnte diese Eingabe nicht senden. Sprachsteuerung neu starten.", + "Connecting to Gemini": "Verbindung zu Gemini", + "Gemini returned an oversized message": "Gemini hat eine zu große Nachricht geliefert", + "Gemini Live connection failed. Check network access, then start voice again.": "Verbindung zu Gemini Live fehlgeschlagen. Netzwerkzugang prüfen, dann die Sprachsteuerung neu starten.", + "Gemini Live connection ended. Start voice again for a new conversation.": "Verbindung zu Gemini Live beendet. Für ein neues Gespräch die Sprachsteuerung neu starten.", + "Gemini Live could not start. Start voice again to request a fresh session.": "Gemini Live konnte nicht starten. Sprachsteuerung neu starten, um eine neue Sitzung anzufordern.", + "Start voice again for a fresh token and a new conversation.": "Sprachsteuerung neu starten für ein neues Token und ein neues Gespräch.", + "Start voice again to request a fresh session credential.": "Sprachsteuerung neu starten, um neue Sitzungszugangsdaten anzufordern.", + "Microphone permission was denied. Allow microphone access and try again.": "Mikrofonberechtigung wurde verweigert. Mikrofonzugriff erlauben und erneut versuchen.", + "Gemini connection timed out. Try again.": "Zeitüberschreitung bei der Gemini-Verbindung. Erneut versuchen.", + "Gemini voice could not start": "Gemini-Sprachsteuerung konnte nicht starten", + "Gemini did not finish the interrupted reply. Start voice again.": "Gemini hat die unterbrochene Antwort nicht beendet. Sprachsteuerung neu starten.", + "Gemini session expiring; finishing the current reply": "Gemini-Sitzung läuft ab; die aktuelle Antwort wird beendet", + "Gemini rejected the voice session. Start voice again to request a fresh session.": "Gemini hat die Sprachsitzung abgelehnt. Sprachsteuerung neu starten, um eine neue Sitzung anzufordern.", + "Start voice again for a fresh session credential. If it repeats, check Gemini model and API access.": "Sprachsteuerung neu starten für neue Sitzungszugangsdaten. Falls es sich wiederholt, Gemini-Modell und API-Zugriff prüfen.", + "Gemini action history is full. Start voice again for a fresh session.": "Der Gemini-Aktionsverlauf ist voll. Sprachsteuerung für eine neue Sitzung neu starten.", + "Gemini requested too many simultaneous actions": "Gemini hat zu viele gleichzeitige Aktionen angefordert", + "Voice text command is too long": "Textbefehl für die Sprachsteuerung ist zu lang", + "Gemini voice is not connected": "Gemini-Sprachsteuerung ist nicht verbunden", + "Gemini interrupted input exceeded its buffer or wait limit. Start voice again.": "Unterbrochene Gemini-Eingabe hat ihr Puffer- oder Wartelimit überschritten. Sprachsteuerung neu starten.", + "Gemini Live connection is expiring. Start voice again for a new conversation.": "Die Verbindung zu Gemini Live läuft ab. Für ein neues Gespräch die Sprachsteuerung neu starten.", + "Gemini voice sessions have a time limit. When a session expires, start voice again for a new conversation.": "Gemini-Sprachsitzungen sind zeitlich begrenzt. Wenn eine Sitzung abläuft, die Sprachsteuerung für ein neues Gespräch neu starten.", + "Stopped by voice": "Per Sprache gestoppt", + "Automatic stop is unavailable for this voice connection.": "Automatisches Beenden ist für diese Sprachverbindung nicht verfügbar.", + "Preview is off.": "Vorschau ist aus.", + "Your choice applies now, but could not be saved.": "Die Auswahl gilt jetzt, konnte aber nicht gespeichert werden.", + "MINI": "MINI", + "standard": "Standard", + "mini": "Mini", + "COST UNKNOWN": "KOSTEN UNBEKANNT", + "ChatGPT OAuth session: {responses} response(s), {input} input and {output} output tokens, {captions} caption transcription(s) reported.": "ChatGPT-OAuth-Sitzung: {responses} Antwort(en), {input} Eingabe- und {output} Ausgabe-Tokens, {captions} Untertitel-Transkription(en) gemeldet.", + "USD cost and the API spend cap are unavailable for this auth mode.": "USD-Kosten und das API-Ausgabenlimit sind in diesem Authentifizierungsmodus nicht verfügbar.", + "Usage is incomplete because a response was still in flight when the session ended.": "Die Nutzung ist unvollständig, weil beim Sitzungsende noch eine Antwort unterwegs war.", + "Estimate is incomplete — a response was still in flight when the session ended, so its usage was never reported.": "Schätzung unvollständig – beim Sitzungsende war noch eine Antwort unterwegs, ihre Nutzung wurde daher nie gemeldet.", + "STANDARD": "STANDARD", + "MIC": "MIKRO", + "{n} aircraft": { + "one": "{n} Luftfahrzeug", + "other": "{n} Luftfahrzeuge" + }, + "{n} military aircraft": { + "one": "{n} Militärluftfahrzeug", + "other": "{n} Militärluftfahrzeuge" + }, + "{n} ships": { + "one": "{n} Schiff", + "other": "{n} Schiffe" + }, + "{n} satellites": { + "one": "{n} Satellit", + "other": "{n} Satelliten" + }, + "{n} launches": { + "one": "{n} Start", + "other": "{n} Starts" + }, + "{n} earthquakes": { + "one": "{n} Erdbeben", + "other": "{n} Erdbeben" + }, + "{n} fires": { + "one": "{n} Brand", + "other": "{n} Brände" + }, + "{n} fire perimeters": { + "one": "{n} Brandgrenze", + "other": "{n} Brandgrenzen" + }, + "{n} cyclones": { + "one": "{n} Wirbelsturm", + "other": "{n} Wirbelstürme" + }, + "{n} buses": { + "one": "{n} Bus", + "other": "{n} Busse" + }, + "{n} bike share": { + "one": "{n} Bikesharing-Station", + "other": "{n} Bikesharing-Stationen" + }, + "{n} flock cameras": { + "one": "{n} ALPR-Kamera", + "other": "{n} ALPR-Kameras" + }, + "{n} military bases": { + "one": "{n} Militärstützpunkt", + "other": "{n} Militärstützpunkte" + }, + "{n} datacenters": { + "one": "{n} Rechenzentrum", + "other": "{n} Rechenzentren" + }, + "{n} dams": { + "one": "{n} Staudamm", + "other": "{n} Staudämme" + }, + "{n} air quality": { + "one": "{n} Luftqualitätsstation", + "other": "{n} Luftqualitätsstationen" + }, + "{n} results": { + "one": "{n} Ergebnis", + "other": "{n} Ergebnisse" + }, + "anywhere in the loaded data": "irgendwo in den geladenen Daten", + "in view": "im Bild", + "within {radius} of the view centre": "im Umkreis von {radius} um die Bildmitte", + "within {radius} of {place} (Contacts window)": "im Umkreis von {radius} um {place} (Kontaktfenster)", + "within {radius} of {place}": "im Umkreis von {radius} um {place}", + "within {radius}": "im Umkreis von {radius}", + "over {region}": "über {region}", + "At least {items}": "Mindestens {items}", + "{items} {scope}": "{items} {scope}", + "last 24 h": "letzte 24 h", + "past 30 d and upcoming": "letzte 30 Tage und anstehende", + "the flights layer loads by viewport": "die Ebene „Live-Flüge“ lädt Daten für den jeweiligen Bildausschnitt", + "distance is ground distance, not overhead": "Entfernung am Boden, ohne Berücksichtigung der Höhe", + "the count is a lower bound because at least one layer reached its retained limit.": "die Anzahl ist eine Untergrenze, weil mindestens eine Ebene ihr Speicherlimit erreicht hat.", + "{label} off": "{label} aus", + "{label} loading": "{label} lädt", + "{label} error": "{label}: Fehler", + "counted the first {returned} loaded records": "die ersten {returned} geladenen Einträge gezählt", + "counted {returned} of {total} loaded records": "{returned} von {total} geladenen Einträgen gezählt", + "{layers} just turned on — still loading, counts will rise": "{layers} gerade eingeschaltet – Daten werden noch geladen, die Zahlen steigen", + "Contacts panel snapshot at {time} — the same retained cohort supplies the card, speech, and follow-up memory": "Snapshot des Kontaktbereichs um {time} – dieselbe gespeicherte Auswahl liefert die Sprachkarte, die Sprachausgabe und das Gedächtnis für Folgefragen", + "Contacts panel snapshot — the same retained cohort supplies the card, speech, and follow-up memory": "Snapshot des Kontaktbereichs – dieselbe gespeicherte Auswahl liefert die Sprachkarte, die Sprachausgabe und das Gedächtnis für Folgefragen", + "Fly below {km} km to a covered region": "Unter {km} km Höhe zu einer abgedeckten Region fliegen", + "No feed here yet · {n} regions available": { + "one": "Hier noch kein Feed · {n} Region verfügbar", + "other": "Hier noch kein Feed · {n} Regionen verfügbar" + }, + "degraded": "eingeschränkt", + "stale": "veraltet", + "partial": "teilweise", + "fallback": "Fallback", + "unavailable": "nicht verfügbar", + "Not answered: {layers}": "Keine Antwort: {layers}", + "Toggle voice control": "Sprachsteuerung umschalten", + "Activate to toggle voice": "Aktivieren, um die Sprachsteuerung umzuschalten", + "Voice active": "Sprachsteuerung aktiv", + "Voice could not be started.": "Sprachsteuerung konnte nicht gestartet werden.", + "the active voice session": "die aktive Sprachsitzung", + "{value} feet": "{value} Fuß", + "Fire · FRP {frp} MW": "Brand · FRP {frp} MW", + "The flights layer is not enabled": "Die Ebene „Live-Flüge“ ist nicht aktiviert", + "The military layer is not enabled": "Die Ebene „Militärflüge“ ist nicht aktiviert", + "The satellites layer is not enabled": "Die Ebene „Satelliten“ ist nicht aktiviert", + "The ais-live-vessels layer is not enabled": "Die Ebene „Live-Schiffe“ ist nicht aktiviert", + "Could not enable the requested layer": "Die angeforderte Ebene konnte nicht aktiviert werden", + "Could not disable the requested layer": "Die angeforderte Ebene konnte nicht deaktiviert werden", + "Radio layer unavailable": "Radio-Ebene nicht verfügbar", + "Camera navigation is unavailable in the current view": "Kameranavigation ist in der aktuellen Ansicht nicht verfügbar", + "Camera navigation policy unavailable": "Richtlinie für die Kameranavigation nicht verfügbar", + "Nearest-aircraft selection supports Flights or Military Flights only": "Die Auswahl des nächstgelegenen Luftfahrzeugs unterstützt nur „Live-Flüge“ oder „Militärflüge“", + "Nearest-aircraft selection needs a preset, place name, or latitude and longitude": "Die Auswahl des nächstgelegenen Luftfahrzeugs braucht ein Preset, einen Ortsnamen oder Breite und Länge", + "Could not arrive at the requested place": "Der angeforderte Ort konnte nicht erreicht werden", + "The nearest airborne aircraft could not be selected": "Das nächstgelegene Luftfahrzeug in der Luft konnte nicht ausgewählt werden", + "No flights within {radius} of the current view": "Keine Flüge im Umkreis von {radius} um die Bildmitte", + "No military within {radius} of the current view": "Keine Militärflüge im Umkreis von {radius} um die Bildmitte", + "No satellites within {radius} of the current view": "Keine Satelliten im Umkreis von {radius} um die Bildmitte", + "No vessels within {radius} of the current view": "Keine Schiffe im Umkreis von {radius} um die Bildmitte", + "{layer} is enabled on the {source} feed, but no airborne aircraft is loaded in the {place} view yet": "{layer} ist mit dem Feed {source} aktiviert, aber in der Ansicht {place} ist noch kein Luftfahrzeug in der Luft geladen", + "{layer} is enabled, but no airborne aircraft is loaded in the {place} view yet": "{layer} ist aktiviert, aber in der Ansicht {place} ist noch kein Luftfahrzeug in der Luft geladen", + "{layer} is enabled, but its aircraft feed is unavailable": "{layer} ist aktiviert, aber der Flugzeug-Feed ist nicht verfügbar", + "{layer} is enabled, but its destination refresh did not complete": "{layer} ist aktiviert, aber die Aktualisierung am Ziel wurde nicht abgeschlossen", + "{layer} is enabled, but {source} is unavailable": "{layer} ist aktiviert, aber {source} ist nicht verfügbar", + "Could not arrive at {place}": "{place} konnte nicht erreicht werden", + "{name} is no longer loaded": "{name} ist nicht mehr geladen", + "Unknown target layer: {target}": "Unbekannte Zielebene: {target}", + "nominal": "normal", + "{label} on": "{label} an", + "Frame aircraft": "Luftfahrzeuge ins Bild nehmen", + "Frame military aircraft": "Militärluftfahrzeuge ins Bild nehmen", + "Frame ships": "Schiffe ins Bild nehmen", + "Frame satellites": "Satelliten ins Bild nehmen", + "Frame contacts": "Kontakte ins Bild nehmen", + "{count} aircraft in frame": { + "one": "{count} Luftfahrzeug im Bild", + "other": "{count} Luftfahrzeuge im Bild" + }, + "{count} military aircraft in frame": { + "one": "{count} Militärluftfahrzeug im Bild", + "other": "{count} Militärluftfahrzeuge im Bild" + }, + "{count} ships in frame": { + "one": "{count} Schiff im Bild", + "other": "{count} Schiffe im Bild" + }, + "{count} satellites in frame": { + "one": "{count} Satellit im Bild", + "other": "{count} Satelliten im Bild" + }, + "{count} contacts in frame": { + "one": "{count} Kontakt im Bild", + "other": "{count} Kontakte im Bild" + }, + "Nearest military aircraft": "Nächstgelegene Militärluftfahrzeuge", + "Nearest ships": "Nächstgelegene Schiffe", + "Nearest satellites": "Nächstgelegene Satelliten", + "Nearest aircraft": "Nächstgelegene Luftfahrzeuge", + "{first} and {second}": "{first} und {second}", + "Layer": "Ebene", + "there": "dort", + "Camera range {range}": "Kameraentfernung {range}", + "Within {radius} of the view": "Im Umkreis von {radius} um die Bildmitte", + "Labels on": "Beschriftungen an", + "Framing uses the nearest 80 loaded contacts": "Ins Bild genommen werden höchstens die 80 nächstgelegenen geladenen Kontakte", + "{distance} from {location}": "{distance} von {location} entfernt", + "Altitude {altitude}": "Höhe {altitude}", + "{origin} to {destination}": "{origin} nach {destination}", + "from {origin}": "von {origin}", + "to {destination}": "nach {destination}", + "Registration {registration}": "Kennzeichen {registration}", + "View state": "Ansichtsstatus", + "Marked {count} places": { + "one": "{count} Ort markiert", + "other": "{count} Orte markiert" + }, + "Nothing marked": "Nichts markiert", + "Not found: {places}": "Nicht gefunden: {places}", + "Tracing outlines": "Umrisse werden nachgezeichnet", + "Frame flights": "Flüge ins Bild nehmen", + "Frame military": "Militärflüge ins Bild nehmen", + "Frame vessels": "Schiffe ins Bild nehmen", + "Frame {target}": "{target} ins Bild nehmen", + "Zoom out to globe": "Auf den Globus herauszoomen", + "Open data layers": "Datenebenen öffnen", + "Open or close a panel": "Bereich öffnen oder schließen", + "Change context mode": "Kontextmodus ändern", + "Control cockpit": "Cockpit steuern", + "Change visual style": "Visuellen Stil ändern", + "Adjust HUD": "HUD anpassen", + "Adjust cyber sonar": "Cyber-Sonar anpassen", + "Adjust detection overlay": "Erkennungs-Overlay anpassen", + "Change map stack": "Kartenstapel ändern", + "Adjust post-processing": "Nachbearbeitung anpassen", + "Control scene": "Szene steuern", + "Control CCTV": "CCTV steuern", + "Control radio": "Radio steuern", + "Track contact": "Kontakt verfolgen", + "Stop tracking": "Verfolgung beenden", + "Clear marks": "Markierungen löschen", + "Move camera": "Kamera bewegen", + "Fly route": "Route abfliegen", + "Analyst query": "Analyseabfrage", + "Next ISS pass": "Nächster ISS-Überflug", + "Next satellite pass": "Nächster Satellitenüberflug", + "Mark the map": "Karte markieren", + "Mark {targets} +{more}": "{targets} +{more} markieren", + "Mark {targets}": "{targets} markieren", + "Fly to {place}": "Zu {place} fliegen", + "Fly to Location": "Zu einem Ort fliegen", + "Nearest aircraft to {place}": "Nächstgelegenes Luftfahrzeug bei {place}", + "Nearest aircraft to Here": "Nächstgelegenes Luftfahrzeug hier", + "Layer on": "Ebene an", + "Layer off": "Ebene aus", + "Zoom out": "Herauszoomen", + "Zoom in": "Hineinzoomen", + "Read the view": "Ansicht auslesen", + "Check view state": "Ansichtsstatus prüfen", + "Action": "Aktion", + "Finding places": "Orte werden gesucht", + "Tracing outline": "Umriss wird nachgezeichnet", + "Looking up place": "Ort wird nachgeschlagen", + "Flying": "Im Flug", + "Turning on layer": "Ebene wird eingeschaltet", + "Loading aircraft": "Luftfahrzeuge werden geladen", + "Picking nearest": "Nächstgelegenes wird ausgewählt", + "Working": "In Arbeit", + "in progress": "läuft", + "done": "erledigt", + "failed": "fehlgeschlagen", + "cancelled": "abgebrochen", + "Outline traced": "Umriss nachgezeichnet", + "Outline unavailable": "Umriss nicht verfügbar", + "Source: {label}": "Quelle: {label}", + "WORKING": "IN ARBEIT", + "DONE": "ERLEDIGT", + "Recent speech": "Zuletzt gesagt", + "HERE": "HIER", + "KTS": "KN", + "FT": "FT", + "FLT": "FLÜGE", + "MIL": "MIL", + "SITE": "STANDORTE", + "WX": "WETTER", + "MM": "MM", + "Weather data by Open-Meteo.com": "Wetterdaten von Open-Meteo.com", + "Cockpit briefing pages": "Briefing-Seiten im Cockpit", + "SIG": "SIG", + "LOCAL": "LOKAL", + "ESC EXIT": "ESC BEENDEN", + "C TOGGLE": "C UMSCHALTEN", + "Active weather products": "Aktive Wetterprodukte", + "RECENT IMAGERY": "AKTUELLE BILDDATEN", + "Imagery days in the box": "Bildtage im Bereich", + "Tune available internet radio stations": "Verfügbare Internetradiosender einstellen", + "DIRECTORY: RADIO BROWSER": "VERZEICHNIS: RADIO BROWSER", + "Local RTL-SDR receiver": "Lokaler RTL-SDR-Empfänger", + "LOCAL RTL-SDR": "LOKALES RTL-SDR", + "Chrome/Edge · WebUSB · one local tuner shared by FM and the Local ADS-B layer.": "Chrome/Edge · WebUSB · ein lokaler Tuner, den UKW und die Ebene „Lokales ADS-B“ gemeinsam nutzen.", + "Use your location for faster ADS-B position decoding": "Eigenen Standort für schnellere Dekodierung von ADS-B-Positionen verwenden", + "CHANGE DEVICE": "GERÄT WECHSELN", + "Choose a different RTL-SDR or receiver channel": "Ein anderes RTL-SDR oder einen anderen Empfängerkanal wählen", + "RTL-SDR receiver mode": "RTL-SDR-Empfangsmodus", + "GAIN": "VERST.", + "RTL-SDR tuner gain for the current mode": "Tuner-Verstärkung des RTL-SDR für den aktuellen Modus", + "AUTO": "AUTO", + "MSG/S": "NACHR./S", + "HEARD": "EMPFANGEN", + "POSITIONED": "MIT POSITION", + "FM MHZ": "UKW MHZ", + "TUNE": "ABSTIMMEN", + "Seek broadcast FM stations": "UKW-Rundfunksender suchen", + "◀ SEEK": "◀ SUCHEN", + "SEEK ▶": "SUCHEN ▶", + "SDR VOL": "SDR-LAUTST.", + "Local SDR volume": "Lautstärke des lokalen SDR", + "Cyber": "Cyber", + "Sonar": "Sonar", + "Cyber sonar contact scanner": "Cyber-Sonar-Kontaktscanner", + "Rings": "Ringe", + "Sonar ring count": "Anzahl der Sonarringe", + "Range": "Reichweite", + "Sonar ring range": "Reichweite der Sonarringe", + "Power": "Leistung", + "Sonar intensity": "Sonarintensität", + "Opacity": "Deckkraft", + "Cyber contact and label opacity": "Deckkraft von Cyber-Kontakten und Beschriftungen", + "Minimum opacity for map dots, icons, models, brackets, and labels between sonar passes": "Minimale Deckkraft für Kartenpunkte, Symbole, Modelle, Klammern und Beschriftungen zwischen Sonardurchläufen", + "Sector": "Sektor", + "Sonar sector width": "Breite des Sonarsektors", + "Draw on the world — click vertices, double-click or Enter to finish, Esc to cancel": "Auf die Welt zeichnen – Eckpunkte anklicken, Doppelklick oder Enter zum Beenden, Esc zum Abbrechen", + "Shape": "Form", + "Shape to draw": "Zu zeichnende Form", + "Area": "Fläche", + "Line": "Linie", + "Pin": "Pin", + "Label (optional)": "Beschriftung (optional)", + "Label for the drawn shape": "Beschriftung der gezeichneten Form", + "Colour of the drawn shape": "Farbe der gezeichneten Form", + "Primary": "Primär", + "Amber": "Bernstein", + "Cyan": "Cyan", + "Green": "Grün", + "Red": "Rot", + "Clear": "Leeren", + "Remove every mark from the board": "Alle Markierungen von der Zeichenfläche entfernen", + "Show your own GeoJSON, KML, KMZ or GPX files on the globe (or drop them onto it). File bytes stay on this device; overlays can appear in map screenshots shared with voice.": "Eigene GeoJSON-, KML-, KMZ- oder GPX-Dateien auf dem Globus zeigen (oder auf ihn ziehen). Die Dateien bleiben auf diesem Gerät; Overlays können in Kartenscreenshots erscheinen, die mit der Sprachsteuerung geteilt werden.", + "Import": "Importieren", + "Choose GeoJSON, KML, KMZ or GPX files": "GeoJSON-, KML-, KMZ- oder GPX-Dateien wählen", + "Imported files": "Importierte Dateien", + "ACQUIRING FRAME": "FRAME WIRD ERFASST", + "STREET LEVEL": "STRASSENEBENE", + "Turn Street Level on or off": "Straßenebene ein- oder ausschalten", + "Show the street-level view full screen (Esc shrinks it)": "Ansicht auf Straßenebene im Vollbild zeigen (Esc verkleinert sie)", + "Image fit": "Bildanpassung", + "FIT": "EINPASSEN", + "Fit: show the whole photo, with dark bars where its shape differs from the frame": "Einpassen: ganzes Foto zeigen, mit dunklen Balken, wo seine Form vom Rahmen abweicht", + "FILL": "FÜLLEN", + "Fill: fill the frame edge to edge, cropping the photo": "Füllen: den Rahmen randlos füllen, das Foto wird beschnitten", + "Close the image and deselect it on the map": "Bild schließen und auf der Karte abwählen", + "Close the street-level image": "Bild auf Straßenebene schließen", + "Street-level viewer": "Viewer für Straßenebene", + "Open this image on the provider's site": "Dieses Bild auf der Website des Anbieters öffnen", + "Imagery providers": "Bildanbieter", + "PROVIDERS": "ANBIETER", + "Imagery filters": "Bildfilter", + "IMAGERY": "BILDER", + "Panorama filter": "Panoramafilter", + "ALL": "ALLE", + "FLAT": "FLACH", + "SINCE": "SEIT", + "Toggle straight-down and tilted map views": "Zwischen senkrechter Ansicht und Schrägansicht wechseln", + "Reset map to north up": "Karte nach Norden ausrichten", + "Reset map bearing to north": "Kartenausrichtung auf Norden zurücksetzen", + "Cyclone request timed out": "Zeitüberschreitung bei der Wirbelsturm-Abfrage", + "Malformed cyclone snapshot": "Fehlerhafter Wirbelsturm-Snapshot", + "STALE · Roads: {source} · Flow: cached TomTom tiles": "VERALTET · Straßen: {source} · Verkehrsfluss: TomTom-Kacheln aus dem Cache", + "routing busy — too many routes at once": "Routing-Dienst ausgelastet – zu viele Routen gleichzeitig", + "route proxy error": "Fehler des Routen-Proxys" +} diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index e531a6529..45310db70 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -74,7 +74,6 @@ "Layout": "Disposición", "HUD layout": "Disposición del HUD", "Tactical": "Táctica", - "Operator": "De operador", "Minimal": "Mínima", "Detection Overlay (D)": "Superposición de detección (D)", "Detection overlay": "Superposición de detección", @@ -196,21 +195,13 @@ "AVAILABLE MISSIONS": "MISIONES DISPONIBLES", "SELECT A MISSION TO INSPECT": "SELECCIONA UNA MISIÓN PARA INSPECCIONAR", "LOADING 30-DAY MISSION INDEX": "CARGANDO ÍNDICE DE MISIONES DE 30 DÍAS", - "Open compact Radio controls": "Abrir los controles compactos de Radio", - "Compact Radio controls": "Controles compactos de Radio", "RADIO": "RADIO", - "RADIO READY": "RADIO LISTA", - "Open detailed Radio controls": "Abrir los controles detallados de Radio", - "Close compact Radio controls": "Cerrar los controles compactos de Radio", "ENABLE": "ACTIVAR", "Previous filtered radio station": "Emisora de radio filtrada anterior", - "Previous station": "Emisora anterior", "Play selected radio station": "Reproducir la emisora de radio seleccionada", "Play": "Reproducir", "Next filtered radio station": "Siguiente emisora de radio filtrada", - "Next station": "Siguiente emisora", "VOLUME": "VOLUMEN", - "Compact Radio volume": "Volumen de la Radio compacta", "Cockpit display and Radio controls": "Controles de visualización y Radio de la cabina", "Expand Cockpit display options": "Expandir las opciones de visualización de la cabina", "Cockpit display options": "Opciones de visualización de la cabina", @@ -355,8 +346,6 @@ "selected": "seleccionada", "nearest": "más cercana", "{action} {target} radio station": "{action} la emisora de radio {target}", - "RADIO STATE UNCERTAIN": "ESTADO DE RADIO SIN CONFIRMAR", - "SYNCING DIRECTORY": "SINCRONIZANDO DIRECTORIO", "SYNCING": "SINCRONIZANDO", "station": "emisora", "Ready": "Listo", @@ -380,6 +369,11 @@ "many": "{count} de cámaras cargadas · activa CCTV para activarlas", "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 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", "Detection overlay: {mode}": "Superposición de detección: {mode}", @@ -2442,8 +2436,6 @@ "Exit cockpit to fly to a route": "Sal de la cabina para volar a una ruta", "Exit cockpit to fly to a file": "Sal de la cabina para volar a un archivo", "Exit cockpit to fly to a scene": "Sal de la cabina para volar a una escena", - "Go to expanded Radio section": "Ir a la sección de Radio expandida", - "Expand Radio section in Context": "Expandir la sección de Radio en Contexto", "New scene name": "Nombre de la nueva escena", "Delete scene \"{scene}\" and all shots?": "¿Eliminar la escena \"{scene}\" y todos sus planos?", "Detailed outline unavailable": "Contorno detallado no disponible", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json new file mode 100644 index 000000000..931e7c3ff --- /dev/null +++ b/src/i18n/locales/fr.json @@ -0,0 +1,3000 @@ +{ + "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.", + "Click where the pin goes.": "Cliquez à l'endroit où poser l'épingle.", + "Click {count} more points.": { + "one": "Cliquez pour ajouter encore {count} point.", + "many": "Cliquez pour ajouter encore {count} de points.", + "other": "Cliquez pour ajouter encore {count} points." + }, + "Those points are in a line — move one off it to enclose an area.": "Ces points sont alignés — déplacez-en un hors de la ligne pour délimiter une zone.", + "That line has no length — click somewhere further away.": "Cette ligne n'a pas de longueur — cliquez plus loin.", + " · {max}-point limit reached": " · limite de {max} points atteinte", + "{measure} · double-click or Enter to finish, Backspace undoes, Esc cancels.{full}": "{measure} · double-clic ou Enter pour terminer, Backspace annule le dernier point, Esc annule.{full}", + "drawings on the map": "les dessins sur la carte", + "That shape already has {count} points — finish it or press Backspace.": "Cette forme a déjà {count} points — terminez-la ou appuyez sur Backspace.", + "That point is off the globe — click on the world.": "Ce point est hors du globe — cliquez sur la Terre.", + "That shape could not be placed.": "Impossible de placer cette forme.", + "Could not place the shape: {error}": "Impossible de placer la forme : {error}", + "Board cleared.": "Tableau effacé.", + "{owner} is using the pointer — close it first.": "Pointeur utilisé par : {owner}. Fermez d'abord cet outil.", + "{label} · Detailed outline unavailable": "{label} · Contour détaillé indisponible", + "Flying to Austin, TX...": "En route vers Austin, Texas…", + "Restoring shared view...": "Restauration de la vue partagée…", + "Configuring viewer...": "Configuration de la vue…", + "Loading Google 3D Tiles...": "Chargement de Google 3D Tiles…", + "Loading the keyless globe...": "Chargement du globe sans clé…", + "Google 3D Tiles unavailable ({detail}). Loading the keyless globe...": "Google 3D Tiles indisponible ({detail}). Chargement du globe sans clé…", + "Initializing systems...": "Initialisation des systèmes…", + "Space Missions isolates replay data. Exit the mode to enable {layer}.": "Le mode Missions spatiales isole les données de rejeu. Quittez-le pour activer {layer}.", + "Data attribution": "Attribution des données", + "Data provided by:": "Données fournies par :", + "Close data attribution": "Fermer l'attribution des données", + "Light aircraft": "Avion léger", + "Glider": "Planeur", + "Turboprop": "Turbopropulseur", + "Airliner": "Avion de ligne", + "Widebody": "Gros-porteur", + "Four-engine jet": "Quadriréacteur", + "Helicopter": "Hélicoptère", + "Fast jet": "Avion de chasse", + "Business jet": "Jet d'affaires", + "Drone": "Drone", + "General emergency": "Urgence générale", + "Unlawful interference": "Intervention illicite", + "Radio failure": "Panne radio", + "Downed aircraft": "Aéronef accidenté", + "Minimum fuel": "Carburant minimum", + "Lifeguard (medical priority)": "Lifeguard (priorité médicale)", + "Rasuwagadhi witness video": "Vidéo d'un témoin à Rasuwagadhi", + "Bhote Koshi valley footage": "Images de la vallée de la Bhote Koshi", + "Flood footage": "Images de la crue", + "Compilation and casualty claims": "Compilation et bilans de victimes", + "SOURCE GEOCONFIRMED · WORLD-PINNED": "SOURCE GÉOCONFIRMÉE · ÉPINGLÉE SUR LA CARTE", + "SOURCE CREDITED · LOCATION UNRESOLVED": "SOURCE CRÉDITÉE · LIEU NON DÉTERMINÉ", + "REFERENCE ONLY · CLAIMS UNVERIFIED": "RÉFÉRENCE SEULEMENT · AFFIRMATIONS NON VÉRIFIÉES", + "SOURCE GEOCONFIRMED": "SOURCE GÉOCONFIRMÉE", + "2021 historical reference versus 2026 post-event imagery; changes are cumulative and include the July 2025 Rasuwagadhi flood. Schematic downstream progression, not modeled arrival time; not an official hazard model.": "Référence historique de 2021 comparée aux images postérieures à l'événement de 2026 ; les changements sont cumulés et incluent la crue de Rasuwagadhi de juillet 2025. Progression schématique vers l'aval, pas une heure d'arrivée modélisée ; ce n'est pas un modèle officiel d'aléa.", + "Immediate collapse viewpoint": "Point de vue au pied de l'effondrement", + "Collapse viewpoint": "Vue de l'effondrement", + "Debris-dammed lake": "Lac de barrage formé par les débris", + "Dammed lake": "Lac de barrage", + "Second landslide": "Second glissement de terrain", + "Gyirong border gate": "Poste frontière de Gyirong", + "Border gate": "Poste frontière", + "Timure evidence cluster": "Groupe de preuves de Timure", + "Mailung / Upper Trishuli bridge": "Mailung / pont d'Upper Trishuli", + "Dandagaun viewpoint restaurant": "Restaurant-belvédère de Dandagaun", + "Dandagaun viewpoint": "Belvédère de Dandagaun", + "Bidur - Trishuli bridge": "Pont Bidur - Trishuli", + "Bidur bridge": "Pont de Bidur", + "Devighat / Taadi Khola Bridge": "Devighat / pont de la Taadi Khola", + "Devighat bridge": "Pont de Devighat", + "Facebook player API did not initialize.": "L'API du lecteur Facebook ne s'est pas initialisée.", + "Facebook player API could not load.": "Impossible de charger l'API du lecteur Facebook.", + "Facebook player API timed out.": "Délai dépassé pour l'API du lecteur Facebook.", + "X embed API did not initialize.": "L'API d'intégration X ne s'est pas initialisée.", + "X embed could not load.": "Impossible de charger l'intégration X.", + "X embed timed out.": "Délai dépassé pour l'intégration X.", + "UNAVAILABLE": "INDISPONIBLE", + "TIMEOUT": "DÉLAI DÉPASSÉ", + "BLOCKED": "BLOQUÉ", + "SOURCE": "SOURCE", + "Facebook embed unavailable. The post may be private, removed, region-restricted, or blocked.": "Intégration Facebook indisponible. La publication est peut-être privée, supprimée, restreinte dans votre région ou bloquée.", + "Preloading {provider} media": "Préchargement du média {provider}", + "LOADING X POST…": "CHARGEMENT DU POST X…", + "The X post is unavailable or cannot be embedded.": "Le post X est indisponible ou ne peut pas être intégré.", + "X POST · PROVIDER CONTROLS": "POST X · COMMANDES DU FOURNISSEUR", + "{reason} OPEN ORIGINAL.": "{reason} OUVRIR L'ORIGINAL.", + "X embed unavailable": "Intégration X indisponible", + "FACEBOOK · PROVIDER CONTROLS": "FACEBOOK · COMMANDES DU FOURNISSEUR", + "Facebook embed unavailable": "Intégration Facebook indisponible", + "AUTOPLAY BLOCKED · USE PROVIDER CONTROLS": "LECTURE AUTO BLOQUÉE · UTILISER LES COMMANDES DU FOURNISSEUR", + "Embedded {provider} media for {source}": "Média {provider} intégré pour {source}", + "witness source": "source témoin", + "Witness source": "Source témoin", + "{provider} · EMBEDDED SOURCE": "{provider} · SOURCE INTÉGRÉE", + "OPEN ORIGINAL ↗": "OUVRIR L'ORIGINAL ↗", + "{provider} media: {source}": "Média {provider} : {source}", + "{provider} · PROVIDER CONTROLS": "{provider} · COMMANDES DU FOURNISSEUR", + "YOUTUBE {state} · OPEN ORIGINAL": "YOUTUBE {state} · OUVRIR L'ORIGINAL", + "INITIATION": "DÉCLENCHEMENT", + "DAMMING": "BARRAGE", + "SECONDARY FAILURE": "RUPTURE SECONDAIRE", + "BORDER ARRIVAL": "ARRIVÉE À LA FRONTIÈRE", + "PROPAGATION": "PROPAGATION", + "DOWNSTREAM PASSAGE": "PASSAGE EN AVAL", + "CONSEQUENCE": "CONSÉQUENCES", + "DOWNSTREAM CONSEQUENCE": "CONSÉQUENCES EN AVAL", + "EVIDENCE": "PREUVES", + "{year} HISTORICAL REFERENCE": "RÉFÉRENCE HISTORIQUE {year}", + "{year} POST-EVENT": "APRÈS L'ÉVÉNEMENT {year}", + "{count} GEOLOCATED SOURCE-MAP PLACEMENTS": { + "one": "{count} EMPLACEMENT GÉOLOCALISÉ SUR LA CARTE DES SOURCES", + "many": "{count} D'EMPLACEMENTS GÉOLOCALISÉS SUR LA CARTE DES SOURCES", + "other": "{count} EMPLACEMENTS GÉOLOCALISÉS SUR LA CARTE DES SOURCES" + }, + "NO PREVIEW": "PAS D'APERÇU", + "YouTube source": "Source YouTube", + "Facebook source": "Source Facebook", + "SOURCE LINK": "LIEN DE LA SOURCE", + "{publisher} / OPEN": "{publisher} / OUVRIR", + "Bhote Koshi flood reconstruction controls": "Commandes de la reconstitution de la crue de la Bhote Koshi", + "EVENT RECONSTRUCTION · 26 AUG 2026": "RECONSTITUTION DE L'ÉVÉNEMENT · 26 AOÛT 2026", + "BHOTE KOSHI OUTBURST FLOOD": "CRUE PAR VIDANGE BRUTALE DE LA BHOTE KOSHI", + "Close event layer": "Fermer la couche de l'événement", + "OBSERVED IMAGERY": "IMAGERIE OBSERVÉE", + "SCHEMATIC CORRIDOR": "CORRIDOR SCHÉMATIQUE", + "2021 HISTORICAL REFERENCE / 2026 POST-EVENT": "RÉF. HISTORIQUE 2021 / APRÈS L'ÉVÉNEMENT 2026", + "Historical reference and post-event image split": "Comparaison entre l'image de référence historique et l'image postérieure à l'événement", + "RECONSTRUCTION CLOCK": "HORLOGE DE LA RECONSTITUTION", + "Schematic downstream progression": "Progression schématique vers l'aval", + "Story beat navigation": "Navigation entre les étapes du récit", + "Previous story beat": "Étape précédente du récit", + "CAUSE": "CAUSE", + "CAPTURE TIME UNVERIFIED": "HEURE DE PRISE DE VUE NON VÉRIFIÉE", + "Next story beat": "Étape suivante du récit", + "▶ PLAY": "▶ LIRE", + "Play the next shot and continue through this scene": "Lire le plan suivant et poursuivre la scène", + "▶ PLAY SCENE": "▶ LIRE LA SCÈNE", + "◉ CINEMATIC": "◉ CINÉMATIQUE", + "↺ FULL STORY": "↺ RÉCIT COMPLET", + "↗ OPEN ORIGINAL": "↗ OUVRIR L'ORIGINAL", + "⌖ LOWER GORGE": "⌖ GORGE AVAL", + "FIELD REPORTS · {count}": "RAPPORTS DE TERRAIN · {count}", + "GEOLOCATIONS · {credit}": "GÉOLOCALISATIONS · {credit}", + "↗ OPEN PUBLIC MAP": "↗ OUVRIR LA CARTE PUBLIQUE", + " Clouds are preserved from the source imagery.": " Les nuages proviennent de l'imagerie source.", + "Rasuwagadhi witness": "Témoin de Rasuwagadhi", + "Open original source for {source}; capture time unverified": "Ouvrir la source originale de {source} ; heure de prise de vue non vérifiée", + "Historical reference and post-event image divider": "Séparateur entre l'image de référence historique et l'image postérieure à l'événement", + "A historical reference {before} percent, B post-event {after} percent": "A, référence historique, {before} pour cent ; B, après l'événement, {after} pour cent", + "■ RELEASE CAMERA": "■ LIBÉRER LA CAMÉRA", + "Seek Nepal scene clock": "Parcourir l'horloge de la scène du Népal", + "NEPAL SCENE CLOCK": "HORLOGE DE LA SCÈNE DU NÉPAL", + "… PLAYING SHOT": "… LECTURE DU PLAN", + "▶ PLAY SHOT": "▶ LIRE LE PLAN", + "Ⅱ PAUSE": "Ⅱ PAUSE", + "↺ REPLAY CINEMATIC": "↺ REVOIR LA CINÉMATIQUE", + "↺ REPLAY": "↺ REVOIR", + "… PLAYING SCENE": "… LECTURE DE LA SCÈNE", + "Continue {scene} from next shot": "Reprendre {scene} au plan suivant", + "current scene": "la scène actuelle", + "Replay the full reconstruction": "Revoir toute la reconstitution", + "SCENE SHOT": "PLAN DE SCÈNE", + "TIME UNVERIFIED": "HEURE NON VÉRIFIÉE", + "{count} SOURCES": { + "one": "{count} SOURCE", + "many": "{count} DE SOURCES", + "other": "{count} SOURCES" + }, + "OUTSIDE IMAGERY SWIPE": "HORS DE LA COMPARAISON D'IMAGES", + "{scene} / AUTHORED SHOT": "{scene} / PLAN SCÉNARISÉ", + "NEPAL FLOOD INCIDENT": "CRUE AU NÉPAL", + "Play {shot}": "Lire {shot}", + "current scene shot": "le plan actuel de la scène", + "Play reconstruction": "Lire la reconstitution", + "Load previous scene shot": "Charger le plan précédent de la scène", + "Load next scene shot": "Charger le plan suivant de la scène", + "SCHEMATIC CORRIDOR, NOT MODELED ARRIVAL TIME.": "CORRIDOR SCHÉMATIQUE, PAS UNE HEURE D'ARRIVÉE MODÉLISÉE.", + "Rasuwa · 26 Aug 2026": "Rasuwa · 26 août 2026", + "Event reconstruction": "Reconstitution de l'événement", + "BHOTE KOSHI FLOOD INCIDENT": "CRUE DE LA BHOTE KOSHI", + "NEPAL · 26 AUG 2026": "NÉPAL · 26 AOÛT 2026", + "USGS INITIAL REPORT": "RAPPORT INITIAL DE L'USGS", + "26 AUG 2026 · 08:37 NPT": "26 AOÛT 2026 · 08:37 NPT", + "28.2710° N · 85.5150° E": "28,2710° N · 85,5150° E", + "INITIAL M4.4 · RECLASSIFIED M5.2 LANDSLIDE": "INITIALEMENT M4,4 · RECLASSÉ EN GLISSEMENT DE TERRAIN M5,2", + "USGS EVENT us7000tbwb · OPEN SOURCE": "ÉVÉNEMENT USGS us7000tbwb · OUVRIR LA SOURCE", + "Open the USGS initial report for the 26 August 2026 Nepal landslide": "Ouvrir le rapport initial de l'USGS sur le glissement de terrain du 26 août 2026 au Népal", + "Show the Nepal context callout without a border or country fill": "Afficher l'encart de contexte du Népal sans frontière ni remplissage du pays", + "Introduce Nepal, draw its border, then reveal the Bhote Koshi incident": "Présenter le Népal, tracer sa frontière, puis révéler l'incident de la Bhote Koshi", + "CITIES": "VILLES", + "Keep the regional incident context and introduce nearby city labels": "Garder le contexte régional de l'incident et faire apparaître les noms des villes proches", + "PLACES": "LIEUX", + "Introduce the reconstruction scene key places from trigger to downstream": "Présenter les lieux clés de la reconstitution, du déclenchement jusqu'à l'aval", + "PATH": "TRACÉ", + "Animate the sourced cyan river-centerline route to Trishuli Bazaar": "Animer le tracé cyan sourcé de l'axe de la rivière jusqu'à Trishuli Bazaar", + "OVERVIEW": "VUE D'ENSEMBLE", + "Keep the completed flood path and location labels visible": "Garder visibles le tracé complet de la crue et les noms de lieux", + "RECORD": "RAPPORT", + "Reveal the trigger record while making a slow camera approach": "Révéler le rapport sur l'événement déclencheur pendant une lente approche de la caméra", + "{speed} kn": "{speed} nd", + "That file is not valid JSON.": "Ce fichier n'est pas un JSON valide.", + "That JSON file is not GeoJSON.": "Ce fichier JSON n'est pas du GeoJSON.", + "That FeatureCollection has no features list.": "Cette FeatureCollection n'a pas de liste features.", + "Every GeoJSON feature must have type Feature.": "Chaque entité GeoJSON doit être de type Feature.", + "That GeoJSON has invalid geometries.": "Ce GeoJSON contient des géométries non valides.", + "That GeoJSON contains an unsupported geometry.": "Ce GeoJSON contient une géométrie non prise en charge.", + "That GeoJSON has invalid coordinates.": "Ce GeoJSON contient des coordonnées non valides.", + "That file creates more than {limit} map objects (the limit).": "Ce fichier crée plus de {limit} objets sur la carte (la limite).", + "That file has more than {limit} style definitions and image links after cleanup (the limit).": "Après nettoyage, ce fichier contient plus de {limit} définitions de style et liens d'images (la limite).", + "That file has more than {limit} features (the limit).": "Ce fichier contient plus de {limit} entités (la limite).", + "That file has more than {limit} points (the limit) across {features} features.": "Ce fichier contient plus de {limit} points (la limite) répartis sur {features} entités.", + "That file has more than {limit} points (the limit).": "Ce fichier contient plus de {limit} points (la limite).", + "That KMZ unpacks to more than the import limit.": "Une fois décompressé, ce KMZ dépasse la limite d'importation.", + "That KMZ is damaged and could not be unpacked.": "Ce KMZ est endommagé et n'a pas pu être décompressé.", + "That KMZ is not a readable zip archive.": "Ce KMZ n'est pas une archive zip lisible.", + "That KMZ uses a zip format this import does not read.": "Ce KMZ utilise un format zip que cette importation ne lit pas.", + "That KMZ is encrypted.": "Ce KMZ est chiffré.", + "That KMZ uses a compression method this import does not read.": "Ce KMZ utilise une méthode de compression que cette importation ne lit pas.", + "That KML has invalid geometry bounds.": "Ce KML a des limites de géométrie non valides.", + "That KML has invalid coordinates.": "Ce KML contient des coordonnées non valides.", + "That GPX file has no document.": "Ce fichier GPX n'a pas de document .", + "That {format} file is not valid XML.": "Ce fichier {format} n'est pas un XML valide.", + "That file has an unreadable image or exceeds the 16-megapixel image limit.": "Ce fichier contient une image illisible ou dépasse la limite de 16 mégapixels par image.", + "That KML could not be read.": "Impossible de lire ce KML.", + "That file has invalid point coordinates.": "Ce fichier contient des coordonnées de points non valides.", + "Ground heights are not available for that file. Try again when the map has loaded.": "Les altitudes du sol ne sont pas disponibles pour ce fichier. Réessayez une fois la carte chargée.", + "That file took too long to read.": "La lecture de ce fichier a pris trop de temps.", + "Nothing to import.": "Rien à importer.", + "That file is larger than {size} MiB.": "Ce fichier dépasse {size} Mio.", + "Only GeoJSON, KML, KMZ and GPX files can be imported.": "Seuls les fichiers GeoJSON, KML, KMZ et GPX peuvent être importés.", + "That file has nothing to draw.": "Ce fichier ne contient rien à dessiner.", + "That KMZ holds no KML document.": "Ce KMZ ne contient aucun document KML.", + "That {format} file could not be read.": "Impossible de lire ce fichier {format}.", + "That file has {count} map objects; the limit is {limit}.": "Ce fichier contient {count} objets cartographiques ; la limite est de {limit}.", + "Ground heights could not be read. Try that file again.": "Impossible de lire les altitudes du sol. Réessayez avec ce fichier.", + "That file has invalid point graphics.": "Ce fichier contient des symboles de points non valides.", + "Overpass rate-limited": "Overpass : limite de requêtes atteinte", + "Overpass timed out": "Délai Overpass dépassé", + "Overpass could not complete the query": "Overpass n'a pas pu terminer la requête", + "Map tiles temporarily unavailable": "Tuiles de la carte temporairement indisponibles", + "Mapped names temporarily unavailable": "Noms cartographiés temporairement indisponibles", + "Overpass temporarily unavailable": "Overpass temporairement indisponible", + "Retrying mapped sites…": "Nouvel essai : sites cartographiés…", + "Fetching mapped sites…": "Récupération des sites cartographiés…", + "{reason} — retrying in {seconds}s": "{reason} — nouvel essai dans {seconds} s", + "{reason} — retry pending": "{reason} — nouvel essai en attente", + "Zoom in to search mapped installations": "Zoomez pour rechercher les installations cartographiées", + "Showing cached mapped sites": "Sites cartographiés en cache affichés", + "Mapped sites not loaded": "Sites cartographiés non chargés", + "Mapped sites loaded": "Sites cartographiés chargés", + "Military airfield": "Aérodrome militaire", + "Naval base": "Base navale", + "Firing range": "Champ de tir", + "Military land": "Terrain militaire", + "Mapped installation": "Installation cartographiée", + "Military area": "Zone militaire", + "flight": "vol", + "military flight": "vol militaire", + "satellite": "satellite", + "feed unavailable": "flux indisponible", + "feed stale": "flux obsolète", + "observed or mapped nearby context": "contexte observé ou cartographié à proximité", + "no observed or mapped objects in current feeds": "aucun objet observé ou cartographié dans les flux actuels", + "Draw": "Dessin", + "Directions": "Itinéraire", + "Recent imagery": "Imagerie récente", + "STATION": "STATION", + "Crewed stations and their visiting vehicles": "Stations habitées et leurs véhicules visiteurs", + "NAV": "NAV", + "GNSS navigation — GPS, GLONASS, Galileo": "Navigation GNSS — GPS, GLONASS, Galileo", + "GEO": "GEO", + "Geostationary belt — comms and weather, fixed over the equator": "Ceinture géostationnaire — télécoms et météo, en position fixe au-dessus de l'équateur", + "VISUAL": "VISUEL", + "Brightest naked-eye objects — CelesTrak visual group": "Objets les plus brillants à l'œil nu — groupe visuel CelesTrak", + "COMMS": "TÉLÉCOM", + "Broadband constellation shell — shown only in DENSE mode": "Couche de la constellation haut débit — affichée uniquement en mode DENSE", + "FISHING": "PÊCHE", + "TOWING": "REMORQUAGE", + "DREDGER": "DRAGUE", + "DIVE OPS": "PLONGÉE", + "MILITARY": "MILITAIRE", + "SAILING": "VOILIER", + "PLEASURE": "PLAISANCE", + "PILOT": "PILOTINE", + "SAR": "SAR", + "TUG": "REMORQUEUR", + "PORT TENDER": "SERVITUDE PORT.", + "ANTI-POLLUTION": "ANTIPOLLUTION", + "LAW ENFORCE": "POLICE", + "MEDICAL": "MÉDICAL", + "HIGH-SPEED": "GRANDE VITESSE", + "PASSENGER": "PASSAGERS", + "CARGO": "CARGO", + "TANKER": "NAVIRE-CITERNE", + "OTHER": "AUTRE", + "Included in bundle": "Inclus dans l'archive", + "Missing bundle file — reimport its bundle": "Fichier de l'archive manquant — réimportez l'archive", + "Source configured; file checked when loaded": "Source configurée ; fichier vérifié au chargement", + "Source unavailable": "Source indisponible", + "ENVIRONMENTAL": "ENVIRONNEMENT", + "EARTH WATCH": "VEILLE TERRE", + "ACTIVE EVENTS": "ÉVÉNEMENTS ACTIFS", + "Starting live contacts…": "Démarrage des contacts en direct…", + "Opening space missions…": "Ouverture des missions spatiales…", + "Scanning active events…": "Analyse des événements actifs…", + "Live earthquakes and active fires, from USGS and NASA": "Séismes et feux actifs en direct, via USGS et NASA", + "It feels like a forbidden cockpit—then you realize the sources are public and the data is real.": "On se croirait dans un cockpit interdit — puis on comprend que les sources sont publiques et les données réelles.", + "Working…": "En cours…", + "Could not open that mission{detail}. Retry or explore manually.": "Impossible d'ouvrir cette mission{detail}. Réessayez ou explorez manuellement.", + "This browser is blocking storage, so that could not be saved.": "Ce navigateur bloque le stockage, l'enregistrement a donc échoué.", + "NORMAL": "NORMAL", + "CRT": "CRT", + "NVG": "NVG", + "FLIR": "FLIR", + "ANIME": "ANIME", + "NOIR": "NOIR", + "SNOW": "NEIGE", + "ARCTIC": "ARCTIQUE", + "ANTARCTIC": "ANTARCTIQUE", + "NORTH AMERICA": "AMÉRIQUE DU NORD", + "SOUTH AMERICA": "AMÉRIQUE DU SUD", + "EUROPE": "EUROPE", + "AFRICA": "AFRIQUE", + "ASIA": "ASIE", + "OCEANIA": "OCÉANIE", + "NORTHERN OCEANIC GRID": "GRILLE OCÉANIQUE NORD", + "SOUTHERN OCEANIC GRID": "GRILLE OCÉANIQUE SUD", + "LOADING": "CHARGEMENT", + "DEGRADED": "DÉGRADÉ", + "STALE": "OBSOLÈTE", + "PARTIAL": "PARTIEL", + "FALLBACK": "SECOURS", + "{state} {layers}": "{state} : {layers}", + "view band|STREET": "RUE", + "view band|CITY": "VILLE", + "view band|METRO": "MÉTROPOLE", + "view band|REGIONAL": "RÉGION", + "view band|GLOBAL": "MONDE", + "SUMMARY": "RÉSUMÉ", + "Awaiting telemetry...": "En attente de télémétrie…", + "REC": "REC", + "AIS: --": "AIS : --", + "N/A": "N/D", + "{mode} {band} {locality} | {region} | ALT {alt} | WINDOW {window} | SUN {sun}° | ONA {ona}° | {zone}": "{mode} · {band} · {locality} | {region} | ALT {alt} | FENÊTRE {window} | SOLEIL {sun}° | ONA {ona}° | {zone}", + "NEAR {poi} ({city}) {distance}": "PRÈS DE {poi} ({city}) {distance}", + "SECTOR {lat} {lon}": "SECTEUR {lat} {lon}", + "hemisphere|N": "N", + "hemisphere|S": "S", + "hemisphere|E": "E", + "hemisphere|W": "O", + "POWER UP · {count} KEYS WAITING": { + "one": "ALLUMER · {count} CLÉ EN ATTENTE", + "many": "ALLUMER · {count} DE CLÉS EN ATTENTE", + "other": "ALLUMER · {count} CLÉS EN ATTENTE" + }, + "POWERED UP": "ALLUMÉ", + "Provider Settings does not answer proxied requests": "Les paramètres des fournisseurs ne répondent pas aux requêtes passant par un proxy", + "Provider Settings is disabled while sharing is enabled": "Les paramètres des fournisseurs sont désactivés tant que le partage est activé", + "Provider Settings answers only the machine running the server": "Les paramètres des fournisseurs ne répondent qu'à la machine qui exécute le serveur", + "Provider Settings answers only local hostnames": "Les paramètres des fournisseurs ne répondent qu'aux noms d'hôte locaux", + "Provider Settings requires an exact local Origin": "Les paramètres des fournisseurs exigent un Origin local exact", + "Unrecognized Origin refused": "Origin non reconnu : requête refusée", + "Cross-origin requests are refused": "Les requêtes d'une autre origine sont refusées", + "Content-Type must be application/json": "Le Content-Type doit être application/json", + "Method not allowed": "Méthode non autorisée", + "Request too large": "Requête trop volumineuse", + "Invalid JSON": "JSON non valide", + "Body must be a JSON object of {ENV_VAR: value}": "Le corps doit être un objet JSON de la forme {ENV_VAR: value}", + "No keys provided": "Aucune clé fournie", + "At most {limit} keys per save": "{limit} clés au maximum par enregistrement", + "Unknown key: {name}": "Clé inconnue : {name}", + "{name} must be a string": "{name} : la valeur doit être une chaîne de caractères", + "{name} is empty": "{name} : la valeur est vide", + "{name} is longer than any real key ({limit} max)": "{name} : la valeur est plus longue que toute clé réelle ({limit} max.)", + "{name} may only contain printable characters with no spaces": "{name} : seuls les caractères imprimables sans espace sont acceptés", + "{name} contains a character that is not valid in a key (#, quotes, $, \\, or backtick)": "{name} : un caractère n'est pas valide dans une clé (#, guillemets, $, \\ ou accent grave)", + "{name} is configured outside Provider Settings and can only be changed where it was set": "{name} est configurée hors des paramètres des fournisseurs et ne peut être modifiée qu'à l'endroit où elle a été définie", + "The key was not saved: could not restrict the credential file to your account; nothing was saved": "La clé n'a pas été enregistrée : impossible de restreindre le fichier d'identifiants à votre compte ; rien n'a été enregistré", + "The key was not saved: the existing configuration could not be read, so nothing was changed": "La clé n'a pas été enregistrée : la configuration existante est illisible, rien n'a donc été modifié", + "Could not write the {store} store": "Impossible d'écrire dans le stockage {store}", + "Save failed ({status}).": "Échec de l'enregistrement ({status}).", + "OAuth check failed: {detail}": "Échec de la vérification OAuth : {detail}", + "Could not check ChatGPT sign-in. Try again.": "Impossible de vérifier la connexion ChatGPT. Réessayez.", + "VOICE AUTH · CHATGPT OAUTH": "AUTH. VOCALE · OAUTH CHATGPT", + "VOICE AUTH · API KEY": "AUTH. VOCALE · CLÉ API", + "USE API KEY": "UTILISER LA CLÉ API", + "USE CHATGPT OAUTH": "UTILISER L'OAUTH CHATGPT", + "Use OPENAI_API_KEY for the next cloud voice session": "Utiliser OPENAI_API_KEY pour la prochaine session vocale cloud", + "Use the signed-in local ChatGPT/Codex OAuth session for the next cloud voice session": "Utiliser la session OAuth ChatGPT/Codex locale déjà connectée pour la prochaine session vocale cloud", + "Metered — a billing-enabled account": "Payant à l'usage — compte avec facturation activée", + "Optional paid token — public access works without one": "Jeton payant facultatif — l'accès public fonctionne sans", + "Free key — register, paste, done": "Clé gratuite — inscrivez-vous, collez-la, c'est tout", + "browser-side": "côté navigateur", + "This key runs in the browser by design — restrict it at the provider (see SECURITY.md)": "Cette clé est conçue pour être utilisée dans le navigateur — restreignez son usage chez le fournisseur (voir SECURITY.md)", + "configured externally": "configurée ailleurs", + "Supplied by your environment, Keychain, or launcher — change it where it was set": "Fournie par votre environnement, le Trousseau ou le lanceur — modifiez-la là où elle a été définie", + "MANAGE ↗": "GÉRER ↗", + "GET KEY ↗": "OBTENIR UNE CLÉ ↗", + "{envVar} saved — paste to replace": "{envVar} enregistrée — collez-en une autre pour la remplacer", + "paste {envVar}": "collez {envVar}", + "REMOVE": "SUPPRIMER", + "Remove {title} from this app's saved keys": "Supprimer {title} des clés enregistrées de l'application", + "unsaved provider keys": "les clés de fournisseurs non enregistrées", + "Project keys: {status}": "Clés du projet : {status}", + "your app configuration": "la configuration de votre application", + "your local .env": "votre .env local", + "Saving…": "Enregistrement…", + "Save failed: {detail}": "Échec de l'enregistrement : {detail}", + "Paste at least one key first.": "Collez d'abord au moins une clé.", + "Saved to {store}. Restarting — this page reloads itself.": "Enregistré dans {store}. Redémarrage — cette page se recharge seule.", + "Cloud voice will use OPENAI_API_KEY on the next session.": "La voix cloud utilisera OPENAI_API_KEY à la prochaine session.", + "Checking local ChatGPT OAuth sign-in…": "Vérification de la connexion OAuth ChatGPT locale…", + "Could not check ChatGPT sign-in.": "Impossible de vérifier la connexion ChatGPT.", + "ChatGPT OAuth selected for cloud voice. Your API key stays saved and available.": "OAuth ChatGPT sélectionné pour la voix cloud. Votre clé API reste enregistrée et disponible.", + "ChatGPT sign-in expired. Opening sign-in again…": "La session ChatGPT a expiré. Réouverture de la page de connexion…", + "Opening ChatGPT sign-in in your browser…": "Ouverture de la page de connexion ChatGPT dans votre navigateur…", + "Could not start ChatGPT sign-in on this machine.": "Impossible de lancer la connexion ChatGPT sur cette machine.", + "Finish ChatGPT sign-in in the browser. Waiting for it to complete…": "Terminez la connexion ChatGPT dans le navigateur. En attente de la fin…", + "ChatGPT sign-in timed out. Click {button} to try again.": "Délai de connexion ChatGPT dépassé. Cliquez sur « {button} » pour réessayer.", + "ChatGPT sign-in complete. OAuth will be used for the next cloud voice session.": "Connexion ChatGPT terminée. OAuth sera utilisé pour la prochaine session vocale cloud.", + "Remove this key from your saved configuration?": "Supprimer cette clé de votre configuration enregistrée ?", + "Removed from {store}. Restarting — this page reloads itself.": "Supprimé de {store}. Redémarrage — cette page se recharge seule.", + "The photorealistic 3D planet, place search + Street View": "La planète 3D photoréaliste, la recherche de lieux + Street View", + "Places context + Street View fallback; optional separate key": "Contexte Places + secours Street View ; clé séparée facultative", + "Voice control — API key or ChatGPT OAuth": "Commande vocale — clé API ou OAuth ChatGPT", + "Gemini Live voice control": "Commande vocale Gemini Live", + "Live ships, worldwide": "Navires en direct, dans le monde entier", + "Live active-fire detections": "Détections de feux actifs en direct", + "Real live traffic (keyless runs a simulation)": "Trafic réel en direct (sans clé : simulation)", + "Bing imagery map stacks + world terrain": "Fonds de carte Bing + relief mondial", + "More flight-polling credits (anonymous works without)": "Plus de crédits d'interrogation des vols (fonctionne sans, en anonyme)", + "Street-level imagery and coverage in the Street Level layer. Free: register an app in the Mapillary developer dashboard and paste its Client Token": "Images et couverture au niveau de la rue dans la couche Niveau rue. Gratuit : enregistrez une application dans le tableau de bord développeur de Mapillary et collez son Client Token", + "Public space-missions access needs no key. Leave blank for standard use; paid tokens only raise request limits.": "L'accès public aux missions spatiales ne nécessite pas de clé. Laissez vide pour un usage standard ; les jetons payants ne font qu'augmenter les limites de requêtes.", + "Air quality request failed": "Échec de la requête sur la qualité de l'air", + "No readings available": "Aucune mesure disponible", + "No current readings": "Aucune mesure actuelle", + "{count} stations reporting": { + "one": "{count} station transmet", + "many": "{count} de stations transmettent", + "other": "{count} stations transmettent" + }, + "Highest air-quality readings in view": "Mesures de qualité de l'air les plus élevées dans la vue", + "Low": "Faible", + "Low health risk": "Risque faible pour la santé", + "Moderate": "Modéré", + "Moderate health risk": "Risque modéré pour la santé", + "High": "Élevé", + "High health risk": "Risque élevé pour la santé", + "Very High": "Très élevé", + "Very High health risk": "Risque très élevé pour la santé", + "Air quality": "Qualité de l'air", + "Canada only · ECCC AQHI network": "Canada seulement · réseau CQAS d'ECCC", + "ECCC request timed out": "Délai dépassé pour la requête ECCC", + "Malformed AQHI station response": "Réponse des stations CQAS mal formée", + "No AQHI stations available": "Aucune station CQAS disponible", + "Malformed AQHI observation response": "Réponse des observations CQAS mal formée", + "Camera source temporarily unavailable": "Source de caméras temporairement indisponible", + "Mapped camera locations": "Emplacements de caméras cartographiés", + "SHOW NEAREST": "LA PLUS PROCHE", + "Stop following the current object before navigating to a camera": "Arrêtez de suivre l'objet actuel avant d'aller vers une caméra", + "Move to the nearest loaded camera and show its details": "Aller à la caméra chargée la plus proche et afficher ses détails", + "Camera badges": "Badges de caméras", + "Cyan cameras turn coral when selected. Wedges illustrate mapped direction, not measured coverage. Nearby cameras may be outside the screen.": "Les caméras cyan deviennent corail une fois sélectionnées. Les secteurs illustrent l'orientation cartographiée, pas une couverture mesurée. Des caméras proches peuvent être hors de l'écran.", + "{count} nearby · {onScreen} on screen": { + "one": "{count} à proximité · {onScreen} à l'écran", + "many": "{count} à proximité · {onScreen} à l'écran", + "other": "{count} à proximité · {onScreen} à l'écran" + }, + "{count} nearby": { + "one": "{count} à proximité", + "many": "{count} à proximité", + "other": "{count} à proximité" + }, + "retrying mapped ALPR cameras": "nouvel essai : caméras LAPI cartographiées", + "loading mapped ALPR cameras": "chargement des caméras LAPI cartographiées", + "No ALPR data for this area — US and Canada only": "Aucune donnée LAPI pour cette zone — États-Unis et Canada seulement", + "Zoom in to load mapped cameras": "Zoomez pour charger les caméras cartographiées", + "Showing cached locations": "Emplacements en cache affichés", + "Coverage limited — zoom in": "Couverture limitée — zoomez", + "No ALPR data for this area": "Aucune donnée LAPI pour cette zone", + "None on screen — nearby cameras are outside the view": "Aucune à l'écran — les caméras proches sont hors de la vue", + "ALPR camera": "Caméra LAPI", + "Camera coverage unavailable": "Couverture des caméras indisponible", + "OpenStreetMap · community mapped": "OpenStreetMap · cartographie communautaire", + "Overpass returned an incomplete camera response": "Overpass a renvoyé une réponse incomplète pour les caméras", + "At least {count}": "Au moins {count}", + "CONTEXT READY": "CONTEXTE PRÊT", + "GLOBAL CONTEXT OFF": "CONTEXTE GLOBAL DÉSACTIVÉ", + "SELECT A FLIGHT, VESSEL, OR MAPPED INSTALLATION": "CHOISIR UN VOL, UN NAVIRE OU UNE INSTALLATION CARTOGRAPHIÉE", + "ENABLE TO LOAD OBSERVED / MAPPED PROXIMITY": "ACTIVER POUR VOIR LES CONTACTS PROCHES OBSERVÉS / CARTOGRAPHIÉS", + "Unavailable": "Indisponible", + "Focus {label}": "Centrer sur {label}", + "Global Context navigation": "Navigation du Contexte global", + "Previous — prior visited contact in the 250 km window": "Précédent — contact visité précédemment dans la fenêtre de 250 km", + "PREVIOUS": "PRÉC.", + "FOCUS": "CENTRER", + "Next — nearest unvisited contact in the 250 km window": "Suivant — contact non visité le plus proche dans la fenêtre de 250 km", + "NEXT": "SUIV.", + "Named areas ({n})": "Zones nommées ({n})", + "{distance} FLIGHT / VESSEL WINDOW": "FENÊTRE VOLS / NAVIRES DE {distance}", + "Open-source mapped/observed context. Missing broadcasts, unloaded map areas, or unmapped sites are not evidence of absence.": "Contexte cartographié/observé en source ouverte. Des diffusions manquantes, des zones de carte non chargées ou des sites non cartographiés ne prouvent pas une absence.", + "mapped matches within {km} km of the subject": "correspondances cartographiées à moins de {km} km de la cible", + "none mapped within {km} km; not a complete 250 km survey": "aucune cartographiée à moins de {km} km ; pas un relevé complet sur 250 km", + "mapped matches from the loaded viewport": "correspondances cartographiées dans la vue chargée", + "viewport feed is not a complete 250 km survey": "les données de la vue ne sont pas un relevé complet sur 250 km", + "HDG {value}°": "HDG {value}°", + "BRG {value}°": "BRG {value}°", + "BRG —": "BRG —", + "CRS {value}°": "CRS {value}°", + "CURRENT VIEWPORT ONLY": "VUE ACTUELLE SEULEMENT", + "WITHIN {km} KM": "À MOINS DE {km} KM", + "Flights": "Vols", + "Military flights": "Vols militaires", + "AIS vessels": "Navires AIS", + "Mapped installations": "Installations cartographiées", + "syncing {count} city feeds...": "synchro de {count} flux de villes…", + "scanning nearby systems...": "analyse des systèmes proches…", + "GBFS fetch error": "erreur de récupération GBFS", + "Station {id}": "Station {id}", + "Station": "Station", + "🚲 {bikes} avail · {docks} docks · {capacity} cap": "🚲 {bikes} dispo · {docks} bornes libres · {capacity} places", + "⚠️ Not installed": "⚠️ Non installée", + "⚠️ Not renting": "⚠️ Location impossible", + "⚠️ Not returning": "⚠️ Retour impossible", + "HDG {value}° (ESTIMATED)": "HDG {value}° (ESTIMÉ)", + "{count} CAMERAS STANDING BY · NO CAMERA SELECTED · CLICK A CAMERA TO ACTIVATE": "{count} CAMÉRAS EN ATTENTE · AUCUNE CAMÉRA SÉLECTIONNÉE · CLIQUER SUR UNE CAMÉRA POUR L'ACTIVER", + "No cameras available in catalog.": "Aucune caméra disponible dans le catalogue.", + "{city} CCTV": "CCTV {city}", + "FOV {value}° (DATASHEET)": "FOV {value}° (FICHE TECHNIQUE)", + "FOV {value}°": "FOV {value}°", + "SPEC FOV {span}° (CAPABILITY)": "FOV CONSTRUCTEUR {span}° (CAPACITÉ)", + "COVERAGE {value}km²": "COUVERTURE {value} km²", + "OVERLAP {count} cams": "CHEVAUCHEMENT {count} cam.", + "ISOLATED VIEW": "VUE ISOLÉE", + "PROJ MONITOR": "PROJ MONITEUR", + "PROJ OFF": "PROJ OFF", + "VIEWSHED": "ZONE VISIBLE", + "CAL {value}": "CAL {value}", + "SRC {value}": "SRC {value}", + "{value} CONTEXT": "CONTEXTE {value}", + "{spec} · SPECS: CCTV-DATABASE.COM": "{spec} · CARACT. : CCTV-DATABASE.COM", + "Atlantic and eastern/central North Pacific; not worldwide cyclone coverage.": "Atlantique et Pacifique Nord oriental/central ; pas une couverture mondiale des cyclones.", + "Cyclone advisories unavailable": "Bulletins cycloniques indisponibles", + "Cyclone data unavailable": "Données cycloniques indisponibles", + "Potential tropical cyclone": "Cyclone tropical potentiel", + "Post-tropical cyclone": "Cyclone post-tropical", + "Hurricane": "Ouragan", + "Tropical storm": "Tempête tropicale", + "Tropical depression": "Dépression tropicale", + "Subtropical storm": "Tempête subtropicale", + "Subtropical depression": "Dépression subtropicale", + "Track and cone match this advisory": "La trajectoire et le cône correspondent à ce bulletin", + "Track/cone awaiting advisory {number}": "Trajectoire/cône en attente du bulletin {number}", + "Track/cone unavailable": "Trajectoire/cône indisponible", + "Cached advisory · stale source": "Bulletin en cache · source obsolète", + "Loading advisories…": "Chargement des bulletins…", + "{count} active storms": { + "one": "{count} cyclone actif", + "many": "{count} de cyclones actifs", + "other": "{count} cyclones actifs" + }, + "Position as of {time}": "Position au {time}", + "Maximum sustained wind: {wind} · Pressure: {pressure}": "Vent maximal soutenu : {wind} · Pression : {pressure}", + "{name} · {classification} · Advisory {number} · issued {time}": "{name} · {classification} · Bulletin {number} · émis le {time}", + "No active NHC/CPHC systems": "Aucun système actif NHC/CPHC", + "Advisories unavailable": "Bulletins indisponibles", + "Cyclones · NHC / CPHC": "Cyclones · NHC / CPHC", + "Atlantic · E/C Pacific": "Atlantique · Pacifique E/C", + "{storms} · {name} selected": "{storms} · {name} sélectionné", + "Official advisory ↗": "Bulletin officiel ↗", + "Active NHC and CPHC cyclone advisories": "Bulletins cycloniques actifs du NHC et du CPHC", + "Wind unavailable": "Vent indisponible", + "Advisory center / forecast track": "Centre selon le bulletin / trajectoire prévue", + "Center-track uncertainty cone": "Cône d'incertitude de la trajectoire du centre", + "Select a storm on the map, or choose a storm in the list to select it and move the camera. Click empty map space to clear the selection. NOAA NHC/CPHC advisory context. The cone describes forecast center-track uncertainty, not storm size or the full hazard area. Forecast point labels are source lead hours, not times computed from advisory issuance. Geometry follows the surface; height is not weather altitude. Consult the official advisory.": "Sélectionnez un cyclone sur la carte, ou choisissez-en un dans la liste pour le sélectionner et déplacer la caméra. Cliquez sur une zone vide de la carte pour effacer la sélection. Contexte des bulletins NOAA NHC/CPHC. Le cône décrit l'incertitude de la trajectoire prévue du centre, pas la taille du cyclone ni toute la zone de danger. Les étiquettes des points de prévision sont les échéances de la source, pas des heures calculées depuis l'émission du bulletin. La géométrie suit la surface ; la hauteur n'est pas une altitude météorologique. Consultez le bulletin officiel.", + "DRIVE": "VOITURE", + "Drive": "En voiture", + "WALK": "À PIED", + "Walk": "À pied", + "BIKE": "VÉLO", + "Bike": "À vélo", + "rate limited": "limite de requêtes atteinte", + "routing service is rate limited": "le service d'itinéraire a atteint sa limite de requêtes", + "route leg too long": "tronçon d'itinéraire trop long", + "route too long": "itinéraire trop long", + "routing busy — too many routes at once": "service d'itinéraire occupé — trop d'itinéraires à la fois", + "route proxy error": "erreur du proxy d'itinéraire", + "{error} — try again in a moment": "{error} — réessayez dans un instant", + "Routing is rate limited — try again in a moment": "Limite de requêtes d'itinéraire atteinte — réessayez dans un instant", + "Routing unavailable (HTTP {status})": "Itinéraire indisponible (HTTP {status})", + "Routing unavailable": "Itinéraire indisponible", + "direction|left": "à gauche", + "direction|right": "à droite", + "direction|slightly left": "légèrement à gauche", + "direction|slightly right": "légèrement à droite", + "direction|sharply left": "franchement à gauche", + "direction|sharply right": "franchement à droite", + "direction|straight": "tout droit", + "direction|around": "en sens inverse", + "fork direction|left": "à gauche", + "fork direction|right": "à droite", + "fork direction|slightly left": "légèrement à gauche", + "fork direction|slightly right": "légèrement à droite", + "fork direction|sharply left": "franchement à gauche", + "fork direction|sharply right": "franchement à droite", + "fork direction|straight": "tout droit", + "lane direction|left": "de gauche", + "lane direction|right": "de droite", + "lane direction|slightly left": "de gauche", + "lane direction|slightly right": "de droite", + "lane direction|sharply left": "la plus à gauche", + "lane direction|sharply right": "la plus à droite", + "lane direction|straight": "qui va tout droit", + "ordinal|{number}st": "{number}re", + "ordinal|{number}nd": "{number}e", + "ordinal|{number}rd": "{number}e", + "ordinal|{number}th": "{number}e", + "Head out on {road}": "Partez par {road}", + "Head out": "Partez", + "Arrive at your destination, on the left": "Vous êtes arrivé, la destination est à gauche", + "Arrive at your destination, on the right": "Vous êtes arrivé, la destination est à droite", + "Arrive at your destination": "Vous êtes arrivé à destination", + "Make a U-turn onto {road}": "Faites demi-tour sur {road}", + "Make a U-turn": "Faites demi-tour", + "Continue straight onto {road}": "Continuez tout droit sur {road}", + "Continue straight": "Continuez tout droit", + "Turn {direction} onto {road}": "Tournez {direction} sur {road}", + "Turn {direction}": "Tournez {direction}", + "Turn onto {road}": "Tournez sur {road}", + "maneuver|Turn": "Tournez", + "Continue {direction} onto {road}": "Continuez {direction} sur {road}", + "Continue {direction}": "Continuez {direction}", + "At the end of the road, turn {direction} onto {road}": "Au bout de la route, tournez {direction} sur {road}", + "At the end of the road, turn {direction}": "Au bout de la route, tournez {direction}", + "At the end of the road, continue onto {road}": "Au bout de la route, continuez sur {road}", + "At the end of the road, continue": "Au bout de la route, continuez", + "Keep {direction} at the fork onto {road}": "À l'embranchement, continuez {direction} sur {road}", + "Keep {direction} at the fork": "À l'embranchement, continuez {direction}", + "Continue at the fork onto {road}": "À l'embranchement, continuez sur {road}", + "Continue at the fork": "À l'embranchement, continuez", + "Merge {direction} onto {road}": "Insérez-vous {direction} sur {road}", + "Merge {direction}": "Insérez-vous {direction}", + "Merge onto {road}": "Insérez-vous sur {road}", + "maneuver|Merge": "Insérez-vous", + "Take the ramp on the {direction} onto {road}": "Prenez la bretelle {direction} vers {road}", + "Take the ramp on the {direction}": "Prenez la bretelle {direction}", + "Take the ramp {direction} onto {road}": "Prenez la bretelle {direction} vers {road}", + "Take the ramp {direction}": "Prenez la bretelle {direction}", + "Take the ramp onto {road}": "Prenez la bretelle vers {road}", + "Take the ramp": "Prenez la bretelle", + "Take the exit on the {direction} onto {road}": "Prenez la sortie {direction} vers {road}", + "Take the exit on the {direction}": "Prenez la sortie {direction}", + "Take the exit {direction} onto {road}": "Prenez la sortie {direction} vers {road}", + "Take the exit {direction}": "Prenez la sortie {direction}", + "Take the exit onto {road}": "Prenez la sortie vers {road}", + "Take the exit": "Prenez la sortie", + "At the roundabout, take the {exit} exit onto {road}": "Au rond-point, prenez la {exit} sortie vers {road}", + "At the roundabout, take the {exit} exit": "Au rond-point, prenez la {exit} sortie", + "Enter the roundabout onto {road}": "Engagez-vous dans le rond-point vers {road}", + "Enter the roundabout": "Engagez-vous dans le rond-point", + "At the roundabout, turn {direction} onto {road}": "Au rond-point, tournez {direction} sur {road}", + "At the roundabout, turn {direction}": "Au rond-point, tournez {direction}", + "At the roundabout, continue onto {road}": "Au rond-point, continuez sur {road}", + "At the roundabout, continue": "Au rond-point, continuez", + "Exit the roundabout onto {road}": "Sortez du rond-point vers {road}", + "Exit the roundabout": "Sortez du rond-point", + "Use the {direction} lane onto {road}": "Prenez la voie {direction} vers {road}", + "Use the {direction} lane": "Prenez la voie {direction}", + "Continue onto {road}": "Continuez sur {road}", + "maneuver|Continue": "Continuez", + "{mode} — reroute for {modeLower}": "{mode} — recalculer l'itinéraire {modeLower}", + "CLICK MAP": "CLIQUER SUR LA CARTE", + "SET A": "PLACER A", + "Click a spot on the globe to place A (click again to cancel)": "Cliquez sur un point du globe pour placer A (cliquez à nouveau pour annuler)", + "Then click the globe to place the start": "Puis cliquez sur le globe pour placer le départ", + "SET B": "PLACER B", + "Click a spot on the globe to place B (click again to cancel)": "Cliquez sur un point du globe pour placer B (cliquez à nouveau pour annuler)", + "Then click the globe to place the destination": "Puis cliquez sur le globe pour placer la destination", + "Swap A and B": "Inverser A et B", + "FLY ···": "SURVOL ···", + "FLYING": "EN SURVOL", + "FLY": "SURVOLER", + "Fly the camera along the route": "Faire voler la caméra le long de l'itinéraire", + "Place A and B first": "Placez d'abord A et B", + "action|CLEAR": "EFFACER", + "Remove the route and both markers": "Supprimer l'itinéraire et les deux marqueurs", + "press Escape twice to leave Draw": "appuyez deux fois sur Esc pour quitter Dessin", + "{name} is active — turn {name} off, then set {endpoint}": "Outil actif : {name} — désactivez {name}, puis placez {endpoint}", + "{name} is active — {leave}, then set {endpoint}": "Outil actif : {name} — {leave}, puis placez {endpoint}", + "Only the first {count} turns of this route are shown": "Seules les {count} premières manœuvres de cet itinéraire sont affichées", + "Turn-by-turn directions": "Instructions pas à pas", + "Another map tool is using clicks — close it, then SET A again": "Un autre outil de carte utilise les clics — fermez-le, puis refaites PLACER A", + "Routing…": "Calcul de l'itinéraire…", + "No route found": "Aucun itinéraire trouvé", + "{distance} · {duration} · {mode} · first {count} turns": "{distance} · {duration} · {mode} · {count} premières manœuvres", + "{distance} · {duration} · {mode}": "{distance} · {duration} · {mode}", + "Click the globe to place {endpoint}": "Cliquez sur le globe pour placer {endpoint}", + "SET B, then click the globe": "PLACER B, puis cliquez sur le globe", + "SET A, then click the globe": "PLACER A, puis cliquez sur le globe", + "Step {step} of {total} · then {leg}": "Étape {step} sur {total} · puis {leg}", + "Step {step} of {total}": "Étape {step} sur {total}", + "Then: {instruction}": "Puis : {instruction}", + "No route found between A and B": "Aucun itinéraire trouvé entre A et B", + "Routing failed: {error}": "Échec du calcul d'itinéraire : {error}", + "Routing timed out": "Délai dépassé pour le calcul d'itinéraire", + "Routing unavailable — no response from the routing service": "Itinéraire indisponible — aucune réponse du service d'itinéraire", + "Earthquake source unavailable": "Source des séismes indisponible", + "Focus fire detection {title}, {details}": "Centrer sur la détection de feu {title}, {details}", + "live feed unavailable": "flux en direct indisponible", + "{value} conf": "conf. {value}", + "{value} ago": "il y a {value}", + "sensor n/a": "capteur n/d", + "FIRE · {frp} MW": "FEU · {frp} MW", + "NIGHT": "NUIT", + "▲ {frp} MW": "▲ {frp} MW", + "FIRE": "FEU", + "FIRES": "FEUX", + "max {value} MW": "max {value} MW", + "new {value}": "dernier il y a {value}", + "{value}°N": "{value}°N", + "{value}°S": "{value}°S", + "{value}°E": "{value}°E", + "{value}°W": "{value}°O", + "just now": "à l'instant", + "<1m ago": "il y a <1 min", + "{count}m ago": "il y a {count} min", + "{count}h ago": "il y a {count} h", + "fire confidence|high": "élevée", + "fire confidence|nominal": "nominale", + "fire confidence|low": "faible", + "age|<1h": "<1 h", + "age|{count}h": "{count} h", + "age|{count}d": "{count} j", + "STALE · cached {age}": "OBSOLÈTE · en cache {age}", + "refreshing...": "actualisation…", + "loading...": "chargement…", + "KEY REQUIRED": "CLÉ REQUISE", + "LIVE · updated {age}": "DIRECT · mis à jour {age}", + "Source snapshot time unavailable": "Heure de l'instantané source indisponible", + "Live data unavailable": "Données en direct indisponibles", + "Intel Picture": "Situation renseignement", + "Kinetic 30d": "Cinétique 30 j", + "Border Crisis": "Crise frontalière", + "Iran Picture": "Situation Iran", + "Iran Kinetic": "Cinétique Iran", + "Yemen FLOT": "FLOT Yémen", + "Latest Drop": "Dernière publication", + "GhostMaps file could not be read": "Impossible de lire le fichier GhostMaps", + "{count} source links ↗ · click card to open the first": { + "one": "{count} lien source ↗ · cliquez sur la fiche pour l'ouvrir", + "many": "{count} de liens sources ↗ · cliquez sur la fiche pour ouvrir le premier", + "other": "{count} liens sources ↗ · cliquez sur la fiche pour ouvrir le premier" + }, + "S2 · {name}": "S2 · {name}", + "Report": "Rapport", + "Open the source for {title}": "Ouvrir la source de {title}", + "GhostMaps catalog unavailable": "Catalogue GhostMaps indisponible", + "LIVE · GitHub {commit} · {message} · checked every {seconds}s": "DIRECT · GitHub {commit} · {message} · vérifié toutes les {seconds} s", + "GitHub {commit} · {message} · reconnecting for live updates": "GitHub {commit} · {message} · reconnexion pour les mises à jour en direct", + "Connecting to the S2 GhostMaps repository…": "Connexion au dépôt S2 GhostMaps…", + "{count} placemarks": { + "one": "{count} repère", + "many": "{count} de repères", + "other": "{count} repères" + }, + "S2 Underground publishes these KMZ products to GitHub. New uploads are detected within seconds and replace the previous edition automatically. Click a marker for its report.": "S2 Underground publie ces produits KMZ sur GitHub. Les nouvelles publications sont détectées en quelques secondes et remplacent automatiquement l'édition précédente. Cliquez sur un marqueur pour voir son rapport.", + "Malformed GhostMaps catalog": "Catalogue GhostMaps mal formé", + "Low-accuracy score < 2%": "Indice de faible précision < 2 %", + "Low-accuracy score 2–10%": "Indice de faible précision 2–10 %", + "Low-accuracy score > 10%": "Indice de faible précision > 10 %", + "GNSS integrity source unavailable": "Source d'intégrité GNSS indisponible", + "Regional sample · 250 nm radius · cells from visited views over 30 min": "Échantillon régional · rayon de 250 NM · cellules des vues visitées sur 30 min", + "Retrying automatically at {time} UTC": "Nouvel essai automatique à {time} UTC", + "Retrying automatically after provider cooldown": "Nouvel essai automatique après la pause du fournisseur", + "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.": "Mise à jour automatique après un déplacement ou un zoom, et une fois par minute. Le flux public couvre un cercle de 250 milles nautiques autour du centre de la vue ; dézoomer n'en fait pas un instantané mondial. Les cellules vides sont hors de la couverture échantillonnée ou manquent d'observations.", + "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.": "Indice de faible précision = 100 × max(0, mauvais − 1) / total : un aéronef est déduit, ce n'est donc pas la proportion brute. Chaque cellule exige {aircraft} aéronefs sur {minutes} minutes. Les anomalies d'intégrité de navigation ne constituent pas une détection de brouillage ou de leurrage.", + "GNSS snapshot needs a view anchor": "L'instantané GNSS nécessite un point d'ancrage de la vue", + "GNSS feed rate limited (HTTP 429)": "Flux GNSS : limite de requêtes atteinte (HTTP 429)", + "Malformed GNSS integrity snapshot": "Instantané d'intégrité GNSS mal formé", + "GNSS integrity snapshot has no observation time": "L'instantané d'intégrité GNSS n'a pas d'heure d'observation", + "installation class|AIRFIELD": "AÉRODROME", + "installation class|NAVAL BASE": "BASE NAVALE", + "installation class|RANGE": "CHAMP DE TIR", + "installation class|MILITARY LAND": "TERRAIN MILITAIRE", + "installation class|BASE": "BASE", + "installation class|BARRACKS": "CASERNE", + "installation class|TRAINING AREA": "ZONE D'ENTRAÎNEMENT", + "installation class|PLACES CANDIDATE": "CANDIDAT PLACES", + "installation class|INSTALLATION": "INSTALLATION", + "No mapped sites within {km} km of the contact": "Aucun site cartographié à moins de {km} km du contact", + "{count} mapped sites within {km} km of the contact": { + "one": "{count} site cartographié à moins de {km} km du contact", + "many": "{count} de sites cartographiés à moins de {km} km du contact", + "other": "{count} sites cartographiés à moins de {km} km du contact" + }, + "No mapped sites in view": "Aucun site cartographié dans la vue", + "{count} mapped sites in view": { + "one": "{count} site cartographié dans la vue", + "many": "{count} de sites cartographiés dans la vue", + "other": "{count} sites cartographiés dans la vue" + }, + "loading mapped installation context": "chargement du contexte de l'installation cartographiée", + "Too many mapped sites in view to list them all": "Trop de sites cartographiés dans la vue pour tous les lister", + "Installation context unavailable": "Contexte des installations indisponible", + "MAPPED INSTALLATION": "INSTALLATION CARTOGRAPHIÉE", + "Unknown": "Inconnu", + "LAUNCH SITE · {site}": "SITE DE LANCEMENT · {site}", + "LAUNCH SITE": "SITE DE LANCEMENT", + "T−{time} · {mission}": "T−{time} · {mission}", + "LAUNCH STANDBY": "LANCEMENT EN ATTENTE", + "LIFTOFF · {mission}": "DÉCOLLAGE · {mission}", + "ASCENT REPLAY · {mission}": "REJEU DE L'ASCENSION · {mission}", + "ASCENT ESTIMATE · {mission}": "ASCENSION ESTIMÉE · {mission}", + "STAGE RE-ENTRY / RECOVERY · {mission}": "RENTRÉE / RÉCUPÉRATION D'ÉTAGE · {mission}", + "ORBIT REPLAY · {mission}": "REJEU DE L'ORBITE · {mission}", + "PAUSED · {title}": "PAUSE · {title}", + "UNSPECIFIED": "NON PRÉCISÉ", + "+{count} additional payload records": "Enregistrements de charge utile supplémentaires : +{count}", + "PAYLOAD DATA UNAVAILABLE": "DONNÉES DE CHARGE UTILE INDISPONIBLES", + "{value} KM DOWNRANGE": "DIST. SOL : {value} KM", + "POSITION UNAVAILABLE": "POSITION INDISPONIBLE", + "FLIGHT {count}": "VOL {count}", + "REUSED": "RÉUTILISÉ", + "NO STAGE RE-ENTRY / RECOVERY DATA": "AUCUNE DONNÉE DE RENTRÉE / RÉCUPÉRATION D'ÉTAGE", + "{count} / 30D": "{count} / 30J", + "NO MISSIONS AVAILABLE IN THE CURRENT 30-DAY WINDOW": "AUCUNE MISSION DISPONIBLE DANS LA FENÊTRE ACTUELLE DE 30 JOURS", + "DATE UNAVAILABLE": "DATE INDISPONIBLE", + "UNSPECIFIED OPERATOR": "OPÉRATEUR NON PRÉCISÉ", + "Select {mission}": "Sélectionner {mission}", + "{value} KM": "{value} KM", + "{value} KM/S": "{value} KM/S", + "Selected Space Mission": "Mission spatiale sélectionnée", + "SELECTED SPACE MISSION": "MISSION SPATIALE SÉLECTIONNÉE", + "Show all missions": "Afficher toutes les missions", + "Deselect mission": "Désélectionner la mission", + "MISSION": "MISSION", + "STATUS · ": "STATUT · ", + "LAUNCH SITE · ": "SITE DE LANCEMENT · ", + "LAUNCH TIME · ": "HEURE DE LANCEMENT · ", + "ORBIT · ": "ORBITE · ", + "ASCENT PATH · ": "TRAJECTOIRE D'ASCENSION · ", + "CURRENT DISTANCE FROM EARTH · ": "DISTANCE ACTUELLE À LA TERRE · ", + "SATELLITE SPEED · ": "VITESSE DU SATELLITE · ", + "PAYLOAD": "CHARGE UTILE", + "NAME": "NOM", + "TYPE": "TYPE", + "DESTINATION": "DESTINATION", + "STAGE / RE-ENTRY / RECOVERY": "ÉTAGE / RENTRÉE / RÉCUPÉRATION", + "STAGE": "ÉTAGE", + "STATUS": "STATUT", + "FINAL POSITION": "POSITION FINALE", + "REPLAY SPEED": "VITESSE DU REJEU", + "Replay speed multiplier": "Multiplicateur de vitesse du rejeu", + "REPLAY ASCENT": "REJOUER L'ASCENSION", + "Previous mission": "Mission précédente", + "PREV": "PRÉC.", + "Next mission": "Mission suivante", + "SHOW ALL / DESELECT": "TOUT AFFICHER / DÉSÉLECTIONNER", + "Pause replay": "Mettre le rejeu en pause", + "Cancel replay": "Annuler le rejeu", + "EST. DOWNRANGE": "DIST. SOL ESTIMÉE", + "PLANNED · ": "PRÉVU · ", + "SUPPLIED TRAJECTORY POINTS": "POINTS DE TRAJECTOIRE FOURNIS", + "RECONSTRUCTED ESTIMATE": "ESTIMATION RECONSTITUÉE", + "Replay the estimated ascent with a following camera": "Rejouer l'ascension estimée avec une caméra de suivi", + "Resume replay": "Reprendre le rejeu", + "T minus {seconds}": "T moins {seconds}", + "Preparing launch site": "Préparation du site de lancement", + "Liftoff": "Décollage", + "Ascent replay": "Rejeu de l'ascension", + "Orbit replay": "Rejeu de l'orbite", + ", paused": ", en pause", + "Malformed launch snapshot": "Instantané des lancements mal formé", + "Heard by your receiver": "Capté par votre récepteur", + "1090 MHz": "1090 MHz", + "978 MHz UAT": "978 MHz UAT", + "browser SDR": "SDR du navigateur", + "decoder feed": "flux du décodeur", + "Your RTL-SDR receiver and decoder feed": "Votre récepteur RTL-SDR et le flux du décodeur", + "Your decoder feed": "Votre flux de décodeur", + "Your RTL-SDR receiver": "Votre récepteur RTL-SDR", + "NO CALLSIGN": "SANS INDICATIF", + "ALT {altitude} FT · GS {speed} KT · TRK {track}": "ALT {altitude} FT · GS {speed} ND · TRK {track}", + "V/S {rate}": "V/S {rate}", + "V/S {rate} FPM": "V/S {rate} FT/MIN", + "POSITION {age}": "POSITION {age}", + "POSITION {seconds} S AGO": "POSITION IL Y A {seconds} S", + "{count} MSGS": { + "one": "{count} MSG", + "many": "{count} DE MSG", + "other": "{count} MSG" + }, + "Decoder feeds": "Flux du décodeur", + "WebUSB + decoder feeds": "WebUSB + flux du décodeur", + "opening receiver": "ouverture du récepteur", + "checking decoder feeds": "vérification des flux du décodeur", + "WebUSB needs desktop Chrome or Edge": "WebUSB nécessite Chrome ou Edge sur ordinateur", + "connect a receiver in Radio": "connectez un récepteur dans Radio", + "receiver is in FM mode": "le récepteur est en mode FM", + "listening": "à l'écoute", + "USB error": "erreur USB", + "Alt unknown": "Alt. inconnue", + "Type unknown": "Type inconnu", + "Reg unknown": "Immat. inconnue", + "Operator unknown": "Opérateur inconnu", + "${amount}K": "{amount} k$", + "${amount}M": "{amount} M$", + "${amount}B": "{amount} Md$", + "fire cause|Human": "humaine", + "fire cause|Natural": "naturelle", + "fire cause|Undetermined": "indéterminée", + "fire cause|Unknown": "inconnue", + "fire behavior|Minimal": "activité minimale", + "fire behavior|Moderate": "activité modérée", + "fire behavior|Active": "activité forte", + "fire behavior|Extreme": "activité extrême", + "Type {level} Incident": "incident de type {level}", + "age|{count}m": "{count} min", + "{acres} ac": { + "one": "{acres} acre", + "many": "{acres} d'acres", + "other": "{acres} acres" + }, + "{percent}% contained": "circonscrit à {percent} %", + "containment unknown": "part circonscrite inconnue", + "{cause} cause": "cause : {cause}", + "{personnel} personnel": { + "one": "{personnel} intervenant", + "many": "{personnel} d'intervenants", + "other": "{personnel} intervenants" + }, + "{county} County": "comté de {county}", + "{cost} to date": "{cost} à ce jour", + "discovered {age} ago": "détecté il y a {age}", + "updated {age} ago": "mis à jour il y a {age}", + "part of {complex}": "fait partie de {complex}", + "InciWeb ↗ · click card to open": "InciWeb ↗ · cliquez sur la fiche pour l'ouvrir", + "FIRE · {name}": "INCENDIE · {name}", + "Unnamed incident": "Incident sans nom", + "Open {title} on InciWeb": "Ouvrir {title} sur InciWeb", + "Perimeter source unavailable": "Source des périmètres indisponible", + "Not contained or unknown": "Non circonscrit ou inconnu", + "Under 50% contained": "Circonscrit à moins de 50 %", + "50–99% contained": "Circonscrit à 50–99 %", + "Fully contained": "Entièrement circonscrit", + "Colour shows reported containment. Perimeters are simplified to about 100 m.": "La couleur indique la part du périmètre déclarée circonscrite. Les périmètres sont simplifiés à environ 100 m.", + "Malformed perimeter snapshot": "Instantané des périmètres mal formé", + "radio tag|All": "Toutes", + "News": "Actualités", + "Talk": "Débats", + "Weather / Emergency": "Météo / Urgences", + "Public Safety": "Sécurité publique", + "Aviation / Marine": "Aviation / Marine", + "Traffic / Transit": "Trafic / Transports", + "Music": "Musique", + "Other": "Autres", + "Directory refresh degraded; showing the previous station catalog.": "Actualisation partielle de l'annuaire ; affichage du catalogue de stations précédent.", + "Radio directory coverage is degraded.": "La couverture de l'annuaire radio est dégradée.", + "Directory refresh failed; showing the previous station catalog.": "Échec de l'actualisation de l'annuaire ; affichage du catalogue de stations précédent.", + "Radio directory is temporarily unavailable.": "L'annuaire radio est temporairement indisponible.", + "radio|NEWS": "ACTUS", + "radio|TALK": "DÉBATS", + "radio|WEATHER": "MÉTÉO", + "radio|SAFETY": "SÉCURITÉ", + "radio|AIR / SEA": "AIR / MER", + "radio|TRANSIT": "TRANSPORTS", + "radio|MUSIC": "MUSIQUE", + "radio|OTHER": "AUTRES", + "Broadcaster stream is unavailable or blocked by the browser.": "Le flux du diffuseur est indisponible ou bloqué par le navigateur.", + "Playback requires a direct click or tap.": "La lecture nécessite un clic ou un appui direct.", + "Broadcaster stream could not be started.": "Impossible de démarrer le flux du diffuseur.", + "Alternative": "Alternatif", + "Ambient": "Ambient", + "Blues": "Blues", + "Classical": "Classique", + "Country": "Country", + "Dance": "Dance", + "Electronic": "Électronique", + "Folk": "Folk", + "Funk": "Funk", + "Hip-Hop": "Hip-Hop", + "House": "House", + "Indie": "Indie", + "Jazz": "Jazz", + "Latin": "Latino", + "Metal": "Metal", + "Oldies": "Oldies", + "Pop": "Pop", + "Punk": "Punk", + "R&B": "R&B", + "Reggae": "Reggae", + "Rock": "Rock", + "Soul": "Soul", + "Techno": "Techno", + "Trance": "Trance", + "World": "Musiques du monde", + "overview": "vue quotidienne", + "cloud unknown": "nébulosité inconnue", + "{percent}% scene cloud": "{percent} % de nuages sur la scène", + "{min}–{max}% scene cloud": "{min}–{max} % de nuages sur la scène", + "Comparison in use by another scene": "Comparaison utilisée par une autre scène", + "Esri map unavailable · no swipe": "Carte Esri indisponible · pas de comparaison", + "Swipe needs a globe map": "La comparaison nécessite une carte du globe", + "Imagery stays on Esri · CLEAR to use Google 3D": "L'imagerie reste sur Esri · EFFACER pour utiliser Google 3D", + "Imagery catalog unavailable": "Catalogue d'images indisponible", + "Catalog truncated · a newer clear day may exist": "Catalogue tronqué · un jour dégagé plus récent peut exister", + "{label} catalog unavailable · {detail}": "Catalogue {label} indisponible · {detail}", + "Daily overview for today may still be empty until the pass": "La vue quotidienne d'aujourd'hui peut rester vide jusqu'au passage du satellite", + "Box and images cleared": "Zone et images effacées", + "MORE DETAIL · 30 m": "PLUS DE DÉTAIL · 30 m", + "Sentinel-2 and Landsat via HLS, 30 m, every 2–6 days": "Sentinel‑2 et Landsat via HLS, 30 m, tous les 2 à 6 jours", + "DAILY OVERVIEW · 250 m": "VUE QUOTIDIENNE · 250 m", + "VIIRS daily mosaic, 250 m, follows broad changes; little detail under 25 km": "Mosaïque quotidienne VIIRS, 250 m, suit les changements à grande échelle ; peu de détail sous 25 km", + "Sentinel-2": "Sentinel‑2", + "Sentinel-2 via HLS": "Sentinel‑2 via HLS", + "Landsat 8/9": "Landsat 8/9", + "Landsat 8/9 via HLS": "Landsat 8/9 via HLS", + "Daily overview": "Vue quotidienne", + "Box edges must be finite": "Coordonnées de la zone non valides", + "Longitudes must be within ±180°": "Les longitudes doivent être comprises entre −180° et +180°", + "Select one side of the dateline": "Ne traversez pas la ligne de changement de date", + "Box must enclose an area": "La zone doit délimiter une surface", + "Add the full Starlink broadband shell (thousands of extra points)": "Ajouter toute la constellation Starlink (des milliers de points en plus)", + "Loading the Starlink shell…": "Chargement de la constellation Starlink…", + "Starlink {detail} — click to retry": "Starlink {detail} — cliquer pour réessayer", + "load failed": "échec du chargement", + "Showing the full Starlink shell — click for the core catalog only": "Constellation Starlink complète affichée — cliquer pour le catalogue de base seulement", + "DENSE ···": "DENSE ···", + "DENSE ✕": "DENSE ✕", + "DENSE": "DENSE", + "CelesTrak unreachable": "CelesTrak injoignable", + "DOCKED · {name} · +{extra}": "AMARRÉ · {name} · +{extra}", + "DOCKED · {name}": "AMARRÉ · {name}", + "{name} needs the Google 3D map: choose it under MAP SOURCE": "{name} nécessite la carte Google 3D : choisissez-la dans SOURCE CARTE", + "zoom in to a street to open {name}": "zoomez sur une rue pour ouvrir {name}", + "loading coverage...": "chargement de la couverture…", + "No street-level imagery within {radius} m of that point": "Aucune image au niveau de la rue à moins de {radius} m de ce point", + "No street-level imagery within {radius} m of the view centre": "Aucune image au niveau de la rue à moins de {radius} m du centre de la vue", + "Zoom in to see street-level coverage": "Zoomez pour voir la couverture au niveau de la rue", + "Point the camera at the ground for street-level coverage": "Orientez la caméra vers le sol pour voir la couverture au niveau de la rue", + "Coverage tile failed": "Échec de la tuile de couverture", + "The photo could not be loaded": "Impossible de charger la photo", + "{name} photo. ← and → step along the sequence. On a 360° photo, drag or use Shift+← →, ↑ ↓ to look around, and + − to zoom": "Photo {name}. ← et → parcourent la séquence. Sur une photo à 360°, faites glisser ou utilisez Shift+← →, ↑ ↓ pour regarder autour, et + − pour zoomer", + "{name} street-level photo": "Photo {name} au niveau de la rue", + "Previous photo in this sequence (←)": "Photo précédente de la séquence (←)", + "Next photo in this sequence (→)": "Photo suivante de la séquence (→)", + "Aim the middle of the view at a street to look for imagery there": "Visez une rue avec le centre de la vue pour y chercher des images", + "KEY REJECTED": "CLÉ REFUSÉE", + "GOOGLE_MAPS_API_KEY is not set": "GOOGLE_MAPS_API_KEY n'est pas définie", + "The Google Maps JavaScript API could not load": "Impossible de charger l'API Google Maps JavaScript", + "Google Street View": "Google Street View", + "GOOGLE STREET VIEW": "GOOGLE STREET VIEW", + "Google refused GOOGLE_MAPS_API_KEY for Street View — check that the Maps JavaScript API is enabled for it and this site is an allowed referrer": "Google a refusé GOOGLE_MAPS_API_KEY pour Street View — vérifiez que l'API Maps JavaScript est activée pour cette clé et que ce site figure parmi les référents autorisés", + "Street View did not answer — check the network, and that the Maps JavaScript API is enabled for GOOGLE_MAPS_API_KEY": "Street View n'a pas répondu — vérifiez le réseau et que l'API Maps JavaScript est activée pour GOOGLE_MAPS_API_KEY", + "click a street to open Street View": "cliquez sur une rue pour ouvrir Street View", + "Street View has no imagery for this place": "Street View n'a pas d'images pour ce lieu", + "KartaView has no position for this photo": "KartaView n'a pas de position pour cette photo", + "The KartaView photo could not be loaded": "Impossible de charger la photo KartaView", + "zoom in to see KartaView coverage": "zoomez pour voir la couverture KartaView", + "no KartaView photos in view": "aucune photo KartaView dans la vue", + "KartaView is rate-limiting requests — they resume on their own": "KartaView limite les requêtes — elles reprendront d'elles-mêmes", + "KartaView’s photos here are archived and cannot be shown": "Les photos KartaView de cet endroit sont archivées et ne peuvent pas être affichées", + "KartaView did not filter this area in time — move the map to ask again": "KartaView n'a pas filtré cette zone à temps — déplacez la carte pour relancer la demande", + "None of KartaView’s photos here pass the imagery filter": "Aucune photo KartaView de cet endroit ne passe le filtre d'images", + "KartaView did not answer in time — try again": "KartaView n'a pas répondu à temps — réessayez", + "KartaView could not be reached — check the network": "KartaView est injoignable — vérifiez le réseau", + "KartaView sent an answer that could not be read": "KartaView a envoyé une réponse illisible", + "KartaView is rate-limiting requests": "KartaView limite les requêtes", + "KartaView sent too much data": "KartaView a envoyé trop de données", + "KartaView is rate-limiting requests — try again in a moment": "KartaView limite les requêtes — réessayez dans un instant", + "KartaView has no such photo": "Cette photo n'existe pas sur KartaView", + "Mapillary rejected MAPILLARY_CLIENT_TOKEN — replace it in Provider Settings": "Mapillary a refusé MAPILLARY_CLIENT_TOKEN — remplacez-le dans les paramètres des fournisseurs", + "Coverage requests are being rate-limited — they resume on their own": "Les requêtes de couverture sont limitées — elles reprendront d'elles-mêmes", + "The Panoramax photo could not be loaded": "Impossible de charger la photo Panoramax", + "Panoramax is busy — coverage resumes on its own": "Panoramax est saturé — la couverture reprendra d'elle-même", + "no Panoramax photos in view": "aucune photo Panoramax dans la vue", + "no Panoramax photos here match the filters": "aucune photo Panoramax de cet endroit ne correspond aux filtres", + "Panoramax sent a map tile that could not be read": "Panoramax a envoyé une tuile de carte illisible", + "This Panoramax photo is no longer available": "Cette photo Panoramax n'est plus disponible", + "zoom in to see Panoramax coverage": "zoomez pour voir la couverture Panoramax", + "Panoramax did not answer in time — try again": "Panoramax n'a pas répondu à temps — réessayez", + "Panoramax could not be reached — check the connection": "Panoramax est injoignable — vérifiez la connexion", + "Panoramax sent too much data": "Panoramax a envoyé trop de données", + "Panoramax is busy": "Panoramax est saturé", + "Panoramax sent an unreadable answer": "Panoramax a envoyé une réponse illisible", + "Sequence images unavailable": "Images de la séquence indisponibles", + "No street-level imagery providers configured": "Aucun fournisseur d'images au niveau de la rue configuré", + "(by upload date)": "(par date de mise en ligne)", + "Open the Street Level panel to view imagery": "Ouvrez le panneau Niveau rue pour voir les images", + "Image could not be opened": "Impossible d'ouvrir l'image", + "TeleGeography load failed": "Échec du chargement de TeleGeography", + "UNAVAILABLE · {source} · Roads unavailable": "INDISPONIBLE · {source} · Routes indisponibles", + "Roads: {source}": "Routes : {source}", + "TomTom roads unavailable while the traffic service is unreachable": "Routes TomTom indisponibles tant que le service de trafic est injoignable", + "Hybrid unavailable while the traffic service is unreachable": "Mode hybride indisponible tant que le service de trafic est injoignable", + "TomTom roads need a TomTom key": "Les routes TomTom nécessitent une clé TomTom", + "Hybrid needs a TomTom key": "Le mode hybride nécessite une clé TomTom", + "Local surface still loading": "Surface locale encore en chargement", + "Partial coverage": "Couverture partielle", + "Detailed roads unavailable": "Routes détaillées indisponibles", + "Reduced detail coverage": "Couverture moins détaillée", + "TomTom roads only, each with its live flow": "Routes TomTom seulement, chacune avec son flux en direct", + "TomTom roads only, each with its live flow (needs a TomTom key)": "Routes TomTom seulement, chacune avec son flux en direct (nécessite une clé TomTom)", + "OpenStreetMap roads, with TomTom flow matched when available": "Routes OpenStreetMap, avec le flux TomTom associé quand il est disponible", + "TomTom roads first; OpenStreetMap fills the rest, simulated": "Routes TomTom d'abord ; OpenStreetMap complète le reste, en simulation", + "TomTom roads first; OpenStreetMap fills the rest, simulated (needs a TomTom key)": "Routes TomTom d'abord ; OpenStreetMap complète le reste, en simulation (nécessite une clé TomTom)", + "Hybrid": "Hybride", + "TomTom key unavailable": "Clé TomTom indisponible", + "TomTom daily budget reached": "Quota quotidien TomTom atteint", + "TomTom upstream unreachable": "Serveur TomTom injoignable", + "TomTom flow unavailable": "Flux TomTom indisponible", + "SIMULATED — {reason}": "SIMULÉ — {reason}", + "Syncing flow · Roads: {source}": "Synchro du flux · Routes : {source}", + "LIVE · Roads: {source} · Roads without flow hidden": "DIRECT · Routes : {source} · Routes sans flux masquées", + "LIVE · Roads: {source} · No flow roads in view": "DIRECT · Routes : {source} · Aucune route avec flux dans la vue", + "LIVE · Roads: {source} · Flow {percent}%": "DIRECT · Routes : {source} · Flux {percent} %", + "SIMULATED · Roads: {source} · Flow {percent}%": "SIMULÉ · Routes : {source} · Flux {percent} %", + "STALE · Roads: {source} · Flow: cached TomTom tiles": "OBSOLÈTE · Routes : {source} · Flux : tuiles TomTom en cache", + "Syncing flow · Roads: OpenStreetMap · Flow: TomTom · Unmatched: simulated": "Synchro du flux · Routes : OpenStreetMap · Flux : TomTom · Non appariées : simulées", + "LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov · Unmatched: simulated": "DIRECT · Routes : OpenStreetMap · Flux : TomTom · couv. {percent} % · Non appariées : simulées", + "LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov": "DIRECT · Routes : OpenStreetMap · Flux : TomTom · couv. {percent} %", + "SIMULATED · Roads: OpenStreetMap · Flow: TomTom (no matches)": "SIMULÉ · Routes : OpenStreetMap · Flux : TomTom (aucune correspondance)", + "SIMULATED — traffic service unreachable": "SIMULÉ — service de trafic injoignable", + "SIMULATED — add TomTom key for live": "SIMULÉ — ajoutez une clé TomTom pour le direct", + "OpenFreeMap tiles rate-limited": "Tuiles OpenFreeMap : limite de requêtes atteinte", + "OpenFreeMap tiles timed out": "Délai dépassé pour les tuiles OpenFreeMap", + "OpenFreeMap tiles unavailable": "Tuiles OpenFreeMap indisponibles", + "occupancy|empty": "vide", + "occupancy|many seats available": "nombreuses places assises libres", + "occupancy|few seats available": "peu de places assises libres", + "occupancy|standing room only": "places debout seulement", + "occupancy|crushed standing room only": "bondé, places debout seulement", + "occupancy|full": "complet", + "occupancy|not accepting passengers": "n'accepte pas de passagers", + "occupancy|not boardable": "embarquement impossible", + "Route {route}": "Ligne {route}", + "Vehicle {vehicle}": "Véhicule {vehicle}", + "vehicle motion|Stopped": "À l'arrêt", + "No recent fix": "Aucune position récente", + "Waiting for update": "En attente de mise à jour", + "Moving": "En mouvement", + "reported {speed} km/h": "vitesse signalée : {speed} km/h", + "reported hdg {heading}°": "cap signalé : {heading}°", + "Last report: stopped at stop {stop}": "Dernier signalement : à l'arrêt {stop}", + "Last report: arriving at stop {stop}": "Dernier signalement : arrivée à l'arrêt {stop}", + "Last report: next stop {stop}": "Dernier signalement : prochain arrêt {stop}", + "Shown at street level · depth not in the feed": "Affiché au niveau de la rue · profondeur absente du flux", + "Reported {seconds} s ago": "Signalé il y a {seconds} s", + "Reported {minutes} min ago": "Signalé il y a {minutes} min", + "shown {seconds} s behind": "affiché avec {seconds} s de retard", + "{reported} (feed time)": "{reported} (heure du flux)", + "Available trail: {minutes} min (limited)": "Trace disponible : {minutes} min (limitée)", + "Available trail: {minutes} min": "Trace disponible : {minutes} min", + "Bus": "Bus", + "Tram": "Tramway", + "Subway": "Métro", + "Train": "Train", + "Ferry": "Ferry", + "Transit vehicle": "Véhicule de transport en commun", + "AIS: {name}": "AIS : {name}", + "{type} SPD: {speed} HDG: {heading}": "{type} SPD : {speed} HDG : {heading}", + "VESSEL": "NAVIRE", + "{speed}KT": "{speed} ND", + "{heading}DEG": "{heading}°", + "POS: LIVE": "POS : DIRECT", + "POS: {time}Z": "POS : {time}Z", + "AIS live load failed": "Échec du chargement AIS en direct", + "awaiting first AIS position…": "en attente de la première position AIS…", + "awaiting usable AIS positions…": "en attente de positions AIS exploitables…", + "awaiting first AIS message…": "en attente du premier message AIS…", + "AISSTREAM_API_KEY not set": "AISSTREAM_API_KEY non définie", + "live feed unsupported": "flux en direct non pris en charge", + "connecting to feed…": "connexion au flux…", + "feed disconnected": "flux déconnecté", + "feed down": "flux hors service", + "feed idle": "flux inactif", + "API key rejected — check AISSTREAM_API_KEY": "Clé API refusée — vérifiez AISSTREAM_API_KEY", + "feed silent {seconds}s — no AIS data": "flux muet depuis {seconds} s — aucune donnée AIS", + "feed silent — no AIS data": "flux muet — aucune donnée AIS", + "feed down — retrying slowly (attempt {attempt})": "flux hors service — nouvel essai espacé (tentative {attempt})", + "reconnecting to feed… (attempt {attempt})": "reconnexion au flux… (tentative {attempt})", + "feed down — retrying slowly": "flux hors service — nouvel essai espacé", + "reconnecting to feed…": "reconnexion au flux…", + "Focus vessel {name}, MMSI {mmsi}": "Centrer sur le navire {name}, MMSI {mmsi}", + "Hidden by this map source · choose a globe map": "Masqué avec cette source de carte · choisissez une carte du globe", + "Weather image HTTP {status}": "Image météo : HTTP {status}", + "Weather HTTP {status}": "Météo : HTTP {status}", + "Frame unavailable; previous observation retained": "Image indisponible ; précédente conservée", + "Weather imagery unavailable": "Imagerie météo indisponible", + "Weather source unavailable; previous observation retained": "Source météo indisponible ; observation précédente conservée", + "Weather unavailable": "Météo indisponible", + "No frame within {gap} min of {time}": "Aucune image à ±{gap} min de {time}", + "No frame within {gap} h of {time}": "Aucune image à ±{gap} h de {time}", + "{hours}h {minutes}m ago": "il y a {hours} h {minutes} min", + "{minutes}m ago": "il y a {minutes} min", + "{state} · {time} · {lag}": "{state} · {time} · {lag}", + "Observed": "Observé", + "History": "Historique", + "Observation unavailable": "Observation indisponible", + "Waiting for observation": "En attente d'observation", + "{detail} · synced": "{detail} · synchro", + "{detail} · nearest": "{detail} · plus proche", + "RADAR REFLECTIVITY · dBZ": "RÉFLECTIVITÉ RADAR · dBZ", + "LIGHTNING DENSITY · 15 min accumulation": "DENSITÉ DE FOUDRE · cumul sur 15 min", + "GLOBAL INFRARED · hourly": "INFRAROUGE MONDIAL · horaire", + "GOES INFRARED · ~5 min": "INFRAROUGE GOES · ~5 min", + "{state}: {time}": "{state} : {time}", + "Latest observation": "Dernière observation", + "frame {index}/{total}": "image {index}/{total}", + "loading": "chargement", + "Observation: unavailable": "Observation : indisponible", + "Contiguous US · gaps ≠ no rain": "États-Unis contigus · lacunes ≠ absence de pluie", + "Americas + Pacific · not individual strikes": "Amériques + Pacifique · pas des impacts individuels", + "Color: strikes/km²/min ×10³": "Couleur : impacts/km²/min ×10³", + "60°S–60°N · typically 2–3 h delayed": "60°S–60°N · retard habituel de 2 à 3 h", + "North America · infrared imagery": "Amérique du Nord · imagerie infrarouge", + "Reduced motion · history playback unavailable": "Animations réduites · lecture de l'historique indisponible", + "Reduced motion · manual history available": "Animations réduites · historique parcourable manuellement", + "Rain radar · US": "Radar de pluie · États-Unis", + "Lightning density · 15 min": "Densité de foudre · 15 min", + "Satellite clouds": "Nuages par satellite", + "Americas + Pacific": "Amériques + Pacifique", + "Global · 60°S–60°N": "Monde · 60°S–60°N", + "North America": "Amérique du Nord", + "Source observations delayed": "Observations de la source en retard", + "Stale source": "Source obsolète", + "Loading next frame…": "Chargement de l'image suivante…", + "Map center outside coverage": "Centre de la carte hors couverture", + "strikes/km²/min ×10³": "impacts/km²/min ×10³", + "N. America": "Am. du Nord", + "Global": "Monde", + "Hourly global mosaic; usually 2–3 hours delayed": "Mosaïque mondiale horaire ; retard habituel de 2 à 3 heures", + "GOES regional clouds; approximately 5-minute updates": "Nuages régionaux GOES ; mise à jour toutes les 5 minutes environ", + "Clouds only": "Nuages seuls", + "Apply the full cloud brightness key; 100% filter strength": "Appliquer pleinement le filtre de luminosité des nuages ; intensité du filtre 100 %", + "Full": "Complète", + "Show the complete infrared image; 0% filter strength": "Afficher l'image infrarouge complète ; intensité du filtre 0 %", + "Soft": "Faible", + "Vivid": "Forte", + "Image opacity; does not alter the observed values": "Opacité de l'image ; ne modifie pas les valeurs observées", + "View US radar": "Voir le radar des États-Unis", + "View Americas & Pacific": "Voir les Amériques et le Pacifique", + "View coverage": "Voir la couverture", + "{value} strikes/km²/min ×10³ (15-minute density)": "{value} impacts/km²/min ×10³ (densité sur 15 minutes)", + "{value} dBZ radar reflectivity": "{value} dBZ de réflectivité radar", + "STALE · cached source metadata": "OBSOLÈTE · métadonnées de la source en cache", + "Map center is outside source coverage": "Le centre de la carte est hors de la couverture de la source", + "NOAA/NWS 15-minute lightning density derived from Vaisala NLDN/GLD360. Coverage 110°E across the Pacific/Americas to 0°, 25°S–80°N. Not a live strike count, global coverage or a safety warning.": "Densité de foudre sur 15 minutes de NOAA/NWS, dérivée de Vaisala NLDN/GLD360. Couverture de 110°E à 0° à travers le Pacifique et les Amériques, 25°S–80°N. Ce n'est ni un décompte d'impacts en direct, ni une couverture mondiale, ni une alerte de sécurité.", + "NOAA MRMS radar echoes indicate precipitation patterns, not rain rate, a storm warning or a future forecast. Native source approximately 1 km; display is limited to level 6. Frames use exact advertised observation times.": "Les échos radar NOAA MRMS indiquent la structure des précipitations, pas l'intensité de la pluie, une alerte d'orage ou une prévision. Résolution native d'environ 1 km ; l'affichage est limité au niveau 6. Les images utilisent les heures d'observation exactes publiées.", + "GOES-19/18 longwave infrared Band 14 regional; NESDIS global longwave mosaic. Clouds only dims everything but bright, cold cloud tops; a brightness filter, not a cloud mask. Coverage and freshness differ by region.": "Infrarouge grandes longueurs d'onde GOES-19/18, bande 14, régional ; mosaïque mondiale grandes longueurs d'onde NESDIS. « Nuages seuls » assombrit tout sauf les sommets nuageux brillants et froids ; c'est un filtre de luminosité, pas un masque de nuages. La couverture et la fraîcheur varient selon la région.", + "REGION": "RÉGION", + "IMAGE": "IMAGE", + "FILTER": "FILTRE", + "Cloud filter strength": "Intensité du filtre de nuages", + "OPACITY": "OPACITÉ", + "Infrared decode failed": "Échec du décodage infrarouge", + "Weather prefetch unavailable": "Préchargement météo indisponible", + "Weather tiles unavailable · previous frame retained": "Tuiles indisp. · image précédente conservée", + "Some weather tiles unavailable": "Certaines tuiles météo indisponibles", + "Malformed weather manifest": "Manifeste météo mal formé", + "Weather request timed out": "Délai dépassé pour la requête météo", + "Temperature field unavailable": "Champ de température indisponible", + "Mean sea level pressure field unavailable": "Champ de pression au niveau de la mer indisponible", + "Wind upstream unavailable": "Source amont du vent indisponible", + "{source} HTTP {status}": "{source} : HTTP {status}", + "Wind": "Vent", + "Wind source unavailable": "Source du vent indisponible", + "Forecast · does not follow history": "Prévision · ne suit pas l'historique", + "Wind motion": "Mouvement du vent", + "Wind speed": "Vitesse du vent", + "Sea-level pressure": "Pression au niveau de la mer", + "Air temperature · 2 m": "Température de l'air · 2 m", + "Global · 1° grid": "Monde · grille de 1°", + "{model} forecast · {time}": "Prévision {model} · {time}", + "Loading forecast": "Chargement de la prévision", + "Preparing flow": "Préparation du flux", + "Selected field unavailable": "Champ sélectionné indisponible", + "Cached forecast · stale": "Prévision en cache · obsolète", + "ECMWF IFS surface forecast": "Prévision de surface ECMWF IFS", + "NOAA GFS surface forecast": "Prévision de surface NOAA GFS", + "Reduced motion": "Animations réduites", + "Resume": "Reprendre", + "Pause": "Pause", + "Pause visual flow; forecast time does not advance with animation": "Mettre en pause le flux visuel ; l'heure de prévision n'avance pas avec l'animation", + "None": "Aucun", + "Speed": "Vitesse", + "Pressure": "Pression", + "Temperature": "Température", + "Wind trails with no field shading": "Traînées de vent sans coloration du champ", + "How hard the surface wind is blowing": "Force du vent en surface", + "Sea-level air pressure: broad highs and lows": "Pression de l'air au niveau de la mer : grands anticyclones et dépressions", + "Air temperature two meters above the surface": "Température de l'air à deux mètres au-dessus de la surface", + "Wind speed units": "Unités de vitesse du vent", + "Read wind at map center": "Lire le vent au centre de la carte", + "Read the forecast at the center of the map without changing selection": "Lire la prévision au centre de la carte sans changer la sélection", + "{model} forecast · {field} ({unit})": "Prévision {model} · {field} ({unit})", + "Valid: {time}": "Valide : {time}", + "preparing": "préparation", + "Issued: {time}": "Émise : {time}", + "Selected field unavailable · wind remains visible": "Champ sélectionné indisponible · le vent reste visible", + "{error} · wind remains visible": "{error} · le vent reste visible", + "Surface wind at 10 m. Approximately 1° global grid. Curves follow the 10 m wind field, lifted 12 km for visibility; display height is not weather altitude. View lighting is for readability. Animation shows flow through one fixed forecast; it does not advance time. Color fields drape the globe basemap or the active photorealistic 3D Tiles.": "Vent de surface à 10 m. Grille mondiale d'environ 1°. Les courbes suivent le champ de vent à 10 m, surélevées de 12 km pour rester visibles ; la hauteur affichée n'est pas une altitude météorologique. L'éclairage de la vue sert à la lisibilité. L'animation montre l'écoulement d'une seule prévision fixe ; elle ne fait pas avancer le temps. Les champs colorés recouvrent le fond de carte du globe ou les 3D Tiles photoréalistes actives.", + "MODEL": "MODÈLE", + "FIELD": "CHAMP", + "UNITS": "UNITÉS", + "MOTION": "ANIMATION", + "Forecast": "Prévision", + "N": "N", + "NNE": "NNE", + "NE": "NE", + "ENE": "ENE", + "E": "E", + "ESE": "ESE", + "SE": "SE", + "SSE": "SSE", + "S": "S", + "SSW": "SSO", + "SW": "SO", + "WSW": "OSO", + "W": "O", + "WNW": "ONO", + "NW": "NO", + "NNW": "NNO", + "No surface reading": "Aucune valeur en surface", + "Aim the map center at Earth": "Visez la Terre avec le centre de la carte", + "A location reading needs a loaded forecast and the Earth at the center of the view.": "Une lecture ponctuelle nécessite une prévision chargée et la Terre au centre de la vue.", + "Interpolated model forecast on an approximately 1° grid. Broad weather patterns, not a street-level measurement.": "Prévision de modèle interpolée sur une grille d'environ 1°. Tendances météo générales, pas une mesure à l'échelle de la rue.", + " · calm": " · calme", + " from {direction}": " · secteur {direction}", + "WIND AT {coordinates}": "VENT À {coordinates}", + "{model} · valid {time}": "{model} · valide {time}", + "Wind speed field unavailable": "Champ de vitesse du vent indisponible", + "Globe imagery unavailable": "Imagerie du globe indisponible", + "Globe field image unavailable": "Image du champ sur le globe indisponible", + "Unknown wind model": "Modèle de vent inconnu", + "Unknown weather overlay": "Surcouche météo inconnue", + "Wind request timed out": "Délai dépassé pour la requête de vent", + "Malformed wind manifest": "Manifeste du vent mal formé", + "Malformed wind grid": "Grille du vent mal formée", + "Error: {message}": "Erreur : {message}", + "Needs {envVars} — add it in Provider Settings": "Nécessite {envVars} — à ajouter dans les paramètres des fournisseurs", + "{label} is unavailable": "{label} est indisponible", + "This map stack": "Ce fond de carte", + "{label} unavailable: {hint}": "{label} indisponible : {hint}", + "Google 3D tiles unavailable — check the key's API restrictions, quota, or network": "Tuiles Google 3D indisponibles — vérifiez les restrictions d'API de la clé, le quota ou le réseau", + "Needs {envVars} — add it in Provider Settings — or a Cesium ion token for the ion-hosted route": "Nécessite {envVars} — à ajouter dans les paramètres des fournisseurs — ou un jeton Cesium ion pour la variante hébergée par ion", + "Exit cockpit to fly to a camera": "Quittez le cockpit pour rejoindre une caméra", + "Exit cockpit to fly to a location": "Quittez le cockpit pour rejoindre un lieu", + "Exit cockpit to fly to a photo": "Quittez le cockpit pour rejoindre une photo", + "Exit cockpit to fly to a shared view": "Quittez le cockpit pour rejoindre une vue partagée", + "Exit cockpit to fly to a target": "Quittez le cockpit pour rejoindre une cible", + "Exit cockpit to fly to a vessel": "Quittez le cockpit pour rejoindre un navire", + "Exit cockpit to fly to a fire": "Quittez le cockpit pour rejoindre un incendie", + "Exit cockpit to fly to a route": "Quittez le cockpit pour rejoindre un itinéraire", + "Exit cockpit to fly to a file": "Quittez le cockpit pour rejoindre un fichier", + "Exit cockpit to fly to a scene": "Quittez le cockpit pour rejoindre une scène", + "Exit cockpit to view weather coverage": "Quittez le cockpit pour voir la couverture météo", + "Exit cockpit to fly to a cyclone": "Quittez le cockpit pour rejoindre un cyclone", + "Exit cockpit to fly to an aircraft": "Quittez le cockpit pour rejoindre un aéronef", + "Exit cockpit to fly to a satellite": "Quittez le cockpit pour rejoindre un satellite", + "Exit cockpit to frame contacts": "Quittez le cockpit pour cadrer les contacts", + "Visible map targets": "Cibles visibles sur la carte", + "Focusing {label}": "Centrage sur {label}", + "Source": "Source", + "Camera ownership changed": "Le contrôle de la caméra a changé", + "Saved project could not be read; storage preserved. Import a valid file to resume saving.": "Le projet enregistré est illisible ; le stockage est conservé. Importez un fichier valide pour reprendre l'enregistrement.", + "Ready": "Prêt", + "Scene layer turned off": "Couche de la scène désactivée", + "Stopped": "Arrêté", + "Scene not saved — existing saved project could not be read. Export edits or import a valid file.": "Scène non enregistrée — le projet enregistré est illisible. Exportez vos modifications ou importez un fichier valide.", + "Scene not saved — {reason}": "Scène non enregistrée — {reason}", + "Scene not saved — browser storage unavailable": "Scène non enregistrée — stockage du navigateur indisponible", + "Loaded: {scene} / {shot}": "Chargé : {scene} / {shot}", + "Cannot update {scene}: shot inventory changed": "Impossible de mettre à jour {scene} : la liste des plans a changé", + "Cannot update {scene}: evidence beats changed": "Impossible de mettre à jour {scene} : les étapes de preuve ont changé", + "Cannot update {scene}: shot bindings are incomplete": "Impossible de mettre à jour {scene} : les liaisons des plans sont incomplètes", + "Updated {count} shots: {scene}": { + "one": "{count} plan mis à jour : {scene}", + "many": "{count} de plans mis à jour : {scene}", + "other": "{count} plans mis à jour : {scene}" + }, + "Appended {count} shots: {scene}": { + "one": "{count} plan ajouté : {scene}", + "many": "{count} de plans ajoutés : {scene}", + "other": "{count} plans ajoutés : {scene}" + }, + "Cannot capture shot: camera not ready": "Capture impossible : caméra non prête", + "Captured: {scene} / {shot}": "Capturé : {scene} / {shot}", + "Select a shot first": "Sélectionnez d'abord un plan", + "Updated: {scene} / {shot}": "Mis à jour : {scene} / {shot}", + "Could not leave scene: {scene}": "Impossible de quitter la scène : {scene}", + "Seeked: {scene} / {shot}": "Position atteinte : {scene} / {shot}", + "Seeking scene clock": "Déplacement dans l'horloge de la scène", + "Camera unavailable — exit cockpit first": "Caméra indisponible — quittez d'abord le cockpit", + "No shots to run": "Aucun plan à lancer", + "Scene transition limit reached — load a shot to reset": "Limite de transitions de scène atteinte — chargez un plan pour réinitialiser", + "Export failed: {error}": "Échec de l'exportation : {error}", + "Project exported": "Projet exporté", + "Importing project": "Importation du projet", + "Imported {file}": "Importé : {file}", + "Import failed: {error}": "Échec de l'importation : {error}", + "Import failed (could not read JSON file)": "Échec de l'importation (fichier JSON illisible)", + "Layers refused: {layers}": "Couches refusées : {layers}", + "Could not exit {mode} — scene layers may be refused": "Impossible de quitter {mode} — les couches de la scène peuvent être refusées", + "Camera move interrupted": "Mouvement de caméra interrompu", + "Scene media timed out": "Délai dépassé pour les médias de la scène", + "Action complete": "Action terminée", + "Action unavailable or cancelled": "Action indisponible ou annulée", + "Scene actions": "Actions de la scène", + "Select a feature or use Tab and Enter to choose an action.": "Sélectionnez un élément ou utilisez Tab et Enter pour choisir une action.", + "Selected feature: {feature}. Choose an action.": "Élément sélectionné : {feature}. Choisissez une action.", + "Running {index}/{total}: {scene} / {shot}": "Exécution {index}/{total} : {scene} / {shot}", + "Scene run complete": "Scène terminée", + "Bhote Koshi Evidence Sequence": "Séquence de preuves de la Bhote Koshi", + "Nepal Flood Incident": "Crue au Népal", + "Global Flights Radar": "Radar mondial des vols", + "Orbital Watch": "Veille orbitale", + "Thermal Threat Board": "Tableau des menaces thermiques", + "City Overload": "Surcharge urbaine", + "Omniscience Pullback": "Recul omniscient", + "Global Incident Context": "Contexte global de l'incident", + "Nepal-Focused Globe Rotation": "Rotation du globe vers le Népal", + "Bhote Koshi Regional Approach": "Approche régionale de la Bhote Koshi", + "Bhote Koshi Nearby Cities": "Villes proches de la Bhote Koshi", + "Bhote Koshi Incident Corridor": "Corridor de l'incident de la Bhote Koshi", + "Bhote Koshi Flood Path": "Tracé de la crue de la Bhote Koshi", + "Bhote Koshi Corridor Overview": "Vue d'ensemble du corridor de la Bhote Koshi", + "Bhote Koshi Upper Valley": "Haute vallée de la Bhote Koshi", + "Upper Valley Collapse": "Effondrement dans la haute vallée", + "Debris-Dammed Lake": "Lac de barrage formé par les débris", + "Nepal-China Border Gate": "Poste frontière Népal-Chine", + "Timure Evidence Cluster": "Groupe de preuves de Timure", + "Syabru Besi Passage": "Passage à Syabru Besi", + "Bidur / Trishuli Consequence": "Conséquences à Bidur / Trishuli", + "Final view": "Vue finale", + "Scene {n}": "Scène {n}", + "Shot {n}": "Plan {n}", + "the title of scene {scene} is empty or longer than {max} characters": "le titre de la scène {scene} est vide ou dépasse {max} caractères", + "the title of shot {shot} in scene {scene} is empty or longer than {max} characters": "le titre du plan {shot} de la scène {scene} est vide ou dépasse {max} caractères", + "the project is larger than 5 MiB": "le projet dépasse 5 Mio", + "the project has too many shots": "le projet contient trop de plans", + "the scene data failed validation": "les données de la scène n'ont pas passé la validation", + "Could not complete this action. Check the file, references and selected assets.": "Impossible de terminer cette action. Vérifiez le fichier, les références et les ressources sélectionnées.", + "{count} scenes": { + "one": "{count} scène", + "many": "{count} de scènes", + "other": "{count} scènes" + }, + "{count} shots": { + "one": "{count} plan", + "many": "{count} de plans", + "other": "{count} plans" + }, + "{count} data packs": { + "one": "{count} paquet de données", + "many": "{count} de paquets de données", + "other": "{count} paquets de données" + }, + "{scene} / {pack}: {status}. File: {path}. {attribution} · {license}": "{scene} / {pack} : {status}. Fichier : {path}. {attribution} · {license}", + "Unavailable layers: {layers}": "Couches indisponibles : {layers}", + "Uses registered content or linked media. Those external files are not included in a scene bundle. Their original notices still apply.": "Utilise du contenu enregistré ou des médias liés. Ces fichiers externes ne sont pas inclus dans une archive de scène. Leurs mentions d'origine restent applicables.", + "{bytes} bundled bytes verified. Files stay in memory for this session. Reimport the bundle after reloading the app.": "{bytes} octets de l'archive vérifiés. Les fichiers restent en mémoire pour cette session. Réimportez l'archive après avoir rechargé l'application.", + "Review scene import": "Vérifier l'importation de scène", + "Nothing is loaded or changed until you apply this file.": "Rien n'est chargé ni modifié tant que vous n'appliquez pas ce fichier.", + "Ready to import": "Prêt à importer", + "Apply replaces the current project. Export your current project first if you want to keep both.": "L'importation remplacera le projet actuel. Exportez d'abord votre projet actuel si vous voulez garder les deux.", + "Apply import": "Appliquer l'importation", + "Import was not applied. The current project may have changed.": "L'importation n'a pas été appliquée. Le projet actuel a peut-être changé.", + "Select a scene and shot first": "Sélectionnez d'abord une scène et un plan", + "Edit scene details": "Modifier les détails de la scène", + "Camera positions use degrees and meters above the ellipsoid. Drafts are validated before they replace your saved scene.": "Les positions de caméra sont en degrés et en mètres au-dessus de l'ellipsoïde. Les brouillons sont validés avant de remplacer votre scène enregistrée.", + "Anchors and data packs": "Ancres et paquets de données", + "Shot camera, timing, packs and actions": "Caméra, minutage, paquets et actions du plan", + "New anchor ID": "ID de la nouvelle ancre", + "Capture camera as anchor": "Créer une ancre à la position de la caméra", + "Anchor added to draft": "Ancre ajoutée au brouillon", + "Set move start to current camera": "Utiliser la vue actuelle comme départ du mouvement", + "Move added to draft; destination remains the shot camera": "Mouvement ajouté au brouillon ; la destination reste la caméra du plan", + "Use ordinary flight": "Utiliser un vol normal", + "Invalid shot JSON": "JSON de plan non valide", + "Apply details": "Appliquer les détails", + "Select a scene first": "Sélectionnez d'abord une scène", + "Share selected scene": "Partager la scène sélectionnée", + "Scene JSON preserves authored settings and attribution. It does not include asset files. Bundles include only pack files you choose here or previously imported bundle files.": "Le JSON de scène conserve les réglages de l'auteur et l'attribution. Il n'inclut pas les fichiers de ressources. Les archives n'incluent que les fichiers de paquets choisis ici ou ceux d'archives déjà importées.", + "Download scene JSON": "Télécharger le JSON de scène", + "Choose data-pack files": "Choisir des fichiers de paquet de données", + "Or choose a data-pack folder": "Ou choisir un dossier de paquet de données", + "Download asset bundle": "Télécharger l'archive de ressources", + "Asset bundle downloaded": "Archive de ressources téléchargée", + "No RTL-SDR device selected": "Aucun appareil RTL-SDR sélectionné", + "WebUSB requires desktop Chrome or Edge on localhost/HTTPS": "WebUSB nécessite Chrome ou Edge sur ordinateur, en localhost/HTTPS", + "RTL-SDR access failed; close other SDR apps and check USB permissions": "Accès au RTL-SDR impossible ; fermez les autres applications SDR et vérifiez les autorisations USB", + "RTL-SDR connection failed": "Échec de la connexion au RTL-SDR", + "Receiving 1090 MHz ADS-B": "Réception ADS-B sur 1090 MHz", + "Receiving broadcast FM": "Réception de la radio FM", + "Connect an RTL-SDR to begin.": "Connectez un RTL-SDR pour commencer.", + "WebUSB is unavailable in this browser": "WebUSB n'est pas disponible dans ce navigateur", + "USB work abandoned after a stalled transfer": "Opération USB abandonnée après un transfert bloqué", + "SDR decoder worker failed": "Échec du processus du décodeur SDR", + "SDR decoder did not start; receiver stopped": "Le décodeur SDR n'a pas démarré ; récepteur arrêté", + "SDR decoder stopped responding; receiver stopped": "Le décodeur SDR ne répond plus ; récepteur arrêté", + "SDR decoder fell behind; receiver stopped": "Le décodeur SDR a pris du retard ; récepteur arrêté", + "WebUSB requires desktop Chrome or Edge": "WebUSB nécessite Chrome ou Edge sur ordinateur", + "Waiting for an RTL-SDR device…": "En attente d'un appareil RTL-SDR…", + "Browser audio is suspended; press Connect again to resume it": "L'audio du navigateur est suspendu ; appuyez à nouveau sur Connecter pour le reprendre", + "RTL-SDR sample stream stopped": "Flux d'échantillons RTL-SDR arrêté", + "Switching to {mode}…": "Passage en {mode}…", + "Browser audio is suspended; select FM again to resume it": "L'audio du navigateur est suspendu ; sélectionnez à nouveau FM pour le reprendre", + "RTL-SDR gain change failed": "Échec du changement de gain du RTL-SDR", + "Tuned to {mhz} MHz FM": "Réglé sur {mhz} MHz FM", + "Seeking up…": "Recherche vers le haut…", + "Seeking down…": "Recherche vers le bas…", + "Seek stopped": "Recherche arrêtée", + "Scanning {mhz} MHz…": "Balayage de {mhz} MHz…", + "Found {mhz} MHz · {snr} dB": "Trouvé {mhz} MHz · {snr} dB", + "FM signal found at {mhz} MHz": "Signal FM trouvé sur {mhz} MHz", + "No FM signal found": "Aucun signal FM trouvé", + "Browser location is unavailable": "La position du navigateur est indisponible", + "Receiver location was not granted": "L'accès à la position du récepteur n'a pas été accordé", + "RTL-SDR disconnected": "RTL-SDR déconnecté", + "Upstream response too large": "Réponse du serveur amont trop volumineuse", + "Vector tile request timed out": "Délai dépassé pour les tuiles vectorielles", + "Vector tiles unavailable": "Tuiles vectorielles indisponibles", + "Vector tile metadata unavailable": "Métadonnées des tuiles vectorielles indisponibles", + "Zoom in for vector tile coverage": "Zoomez pour charger les tuiles vectorielles", + "Saturation": "Saturation", + "Edge Thickness": "Épaisseur des contours", + "Contrast": "Contraste", + "Grain": "Grain", + "Vignette": "Vignettage", + "Pixelation": "Pixellisation", + "Distortion": "Distorsion", + "Instability": "Instabilité", + "Density": "Densité", + "Gain": "Gain", + "Bloom": "Bloom", + "Scanlines": "Lignes de balayage", + "Sensitivity": "Sensibilité", + "WHOT/BHOT": "WHOT/BHOT", + "Ironbow": "Ironbow", + "unsaved CCTV calibration": "la calibration CCTV non enregistrée", + "the open cockpit": "le cockpit ouvert", + "Switching language reloads the page. This will be lost: {items}. Switch anyway?": "Changer de langue recharge la page. Ceci sera perdu : {items}. Changer quand même ?", + "VISUAL PRESETS": "PRÉRÉGLAGES VISUELS", + "LOCATION": "LIEU", + "CONTACTS — nearest planes · vessels · sites": "CONTACTS — avions, navires et sites les plus proches", + "SPACE MISSIONS — launches & orbital assets": "MISSIONS SPATIALES — lancements et objets orbitaux", + "LIVE SIGNALS": "SIGNAUX EN DIRECT", + "Contacts could not complete the requested transition; try again": "Les contacts n'ont pas pu effectuer la transition demandée ; réessayez", + "CCTV layer unavailable": "Couche CCTV indisponible", + "Link shared": "Lien partagé", + "Link copied!": "Lien copié !", + "Copy failed": "Échec de la copie", + "Return map to straight-down view": "Remettre la carte en vue verticale", + "Tilt map to oblique view": "Incliner la carte en vue oblique", + "Reset map to north up. Current heading {heading} degrees": "Réorienter la carte nord en haut. Orientation actuelle : {heading} degrés", + "Tilted view": "Vue inclinée", + "Straight-down view": "Vue verticale", + "North up": "Nord en haut", + "CCTV calibration saved": "Calibration CCTV enregistrée", + "HDG": "HDG", + "PITCH": "INCL.", + "FOV": "FOV", + "RANGE": "PORTÉE", + "HGT": "HAUT.", + "ΔN": "ΔN", + "ΔE": "ΔE", + "{value}m": "{value} m", + "CCTV calibration reset": "Calibration CCTV réinitialisée", + "ADJUST ON": "AJUSTER ON", + "ADJUST": "AJUSTER", + "SNAPSHOT": "INSTANTANÉ", + "LIVE": "DIRECT", + "UPSTREAM": "AMONT", + "SYNTHETIC": "SYNTHÉTIQUE", + "CONFIGURED": "CONFIGURÉ", + "SEED": "CATALOGUE", + "UNKNOWN": "INCONNU", + "OK": "OK", + "ERROR": "ERREUR", + "Live HLS connected": "HLS en direct connecté", + "Live HLS unavailable": "HLS en direct indisponible", + "No stream URL configured": "Aucune URL de flux configurée", + "Live stream connected": "Flux en direct connecté", + "Snapshot feed connected": "Flux d'instantanés connecté", + "Media fetch failed": "Échec de la récupération du média", + "Upstream snapshot active": "Instantané amont actif", + "Fallback Street View frame": "Image Street View de secours", + "Upstream unavailable": "Source amont indisponible", + "No source configured": "Aucune source configurée", + "Configured source": "Source configurée", + "Configured Source": "Source configurée", + "Synthetic fallback": "Secours synthétique", + "Upstream HTTP {status}": "Source amont : HTTP {status}", + "Unexpected media type {type}": "Type de média inattendu : {type}", + "SOURCE · UNKNOWN": "SOURCE · INCONNUE", + "FRAME · LOADING": "IMAGE · CHARGEMENT", + "FRAME · UNAVAILABLE": "IMAGE · INDISPONIBLE", + "CALIBRATED": "CALIBRÉE", + "CURATED": "VÉRIFIÉE", + "RAW PRIOR": "ESTIM. INITIALE", + "CCTV ON": "CCTV ON", + "CCTV OFF": "CCTV OFF", + "VIEWSHED ON": "ZONE VISIBLE ON", + "COVERAGE ON": "COUVERTURE ON", + "COVERAGE OFF": "COUVERTURE OFF", + "AUTO HOP ON": "SAUT AUTO ON", + "AUTO HOP OFF": "SAUT AUTO OFF", + "PROJECTION ON": "PROJECTION ON", + "PROJECTION OFF": "PROJECTION OFF", + "CAL · EDITED (UNSAVED)": "CAL · MODIFIÉE (NON ENREGISTRÉE)", + "CAL · {badge}": "CAL · {badge}", + "MONITOR": "MONITEUR", + "OFF": "OFF", + "{city} · HDG {heading} · FOV {fov} · RANGE {range}m · {projection}{calBadge} · {provider}{status}": "{city} · HDG {heading} · FOV {fov} · PORTÉE {range} m · {projection}{calBadge} · {provider}{status}", + "{count} cameras loaded · click a camera to activate": { + "one": "{count} caméra chargée · cliquer sur une caméra pour l'activer", + "many": "{count} de caméras chargées · cliquer sur une caméra pour l'activer", + "other": "{count} caméras chargées · cliquer sur une caméra pour l'activer" + }, + "{count} cameras loaded · enable CCTV to activate": { + "one": "{count} caméra chargée · activer CCTV pour l'utiliser", + "many": "{count} de caméras chargées · activer CCTV pour les utiliser", + "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é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.", + "No summary available.": "Aucun résumé disponible.", + "loading frames": "chargement des images", + "camera grid ready": "grille de caméras prête", + "Clearing selected data layers": "Effacement des couches de données sélectionnées", + "Clear selected data layers": "Effacer les couches de données sélectionnées", + "CONDITIONS UNKNOWN": "CONDITIONS INCONNUES", + "CLEAR": "DÉGAGÉ", + "PARTLY CLOUDY": "PARTIELLEMENT NUAGEUX", + "OVERCAST": "COUVERT", + "FOG": "BROUILLARD", + "DRIZZLE": "BRUINE", + "RAIN": "PLUIE", + "RAIN SHOWERS": "AVERSES DE PLUIE", + "SNOW SHOWERS": "AVERSES DE NEIGE", + "THUNDERSTORM": "ORAGE", + "MIXED CONDITIONS": "CONDITIONS VARIABLES", + "CYCLE ON": "DÉFIL. ON", + "CYCLE OFF": "DÉFIL. OFF", + "Stop automatic page cycling. Previous, Next, and the SIG/NEWS/LOCAL tabs stay available.": "Arrêter le défilement automatique des pages. Précédent, Suivant et les onglets SIG/ACTUS/LOCAL restent disponibles.", + "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", + "RESOLVING REGION": "IDENTIFICATION DE LA RÉGION", + "REGION UNAVAILABLE": "RÉGION INDISPONIBLE", + "NO RECENT LOCATION MATCHES": "AUCUNE ACTU RÉCENTE POUR CE LIEU", + "{domain} · {age}": "{domain} · {age}", + "{value} KM/H": "{value} KM/H", + "CLOUD {pct}%": "NÉBULOSITÉ {pct} %", + "CLOUD UNKNOWN": "NÉBULOSITÉ INCONNUE", + "{source} · LOCATION QUERY": "{source} · REQUÊTE DE LIEU", + "REGIONAL NEWS": "ACTUS RÉGIONALES", + "{label} · {radius} AIR/SEA WINDOW": "{label} · AIR/MER {radius}", + "{scope} · INSTALLATIONS {coverage}": "{scope} · INSTALLATIONS : {coverage}", + "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", + "CONTACT LOST · {subject}": "CONTACT PERDU · {subject}", + "SUBJECT LEFT ITS FEED · READOUT HOLDING LAST KNOWN": "CIBLE ABSENTE DU FLUX · DERNIER RELEVÉ CONSERVÉ", + "{cohort} · {contact}": "{cohort} · {contact}", + "NO AVAILABLE EXAMPLE": "AUCUN EXEMPLE DISPONIBLE", + "{cohort}, Unavailable": "{cohort}, indisponible", + "AHEAD": "DEVANT", + "{side} {angle}": "{side} {angle}", + "L": "G", + "R": "D", + "{count} INPUTS UNKNOWN · NOT AN ALL-CLEAR": { + "one": "{count} ENTRÉE INCONNUE · PAS UN FEU VERT", + "many": "{count} D'ENTRÉES INCONNUES · PAS UN FEU VERT", + "other": "{count} ENTRÉES INCONNUES · PAS UN FEU VERT" + }, + "AVAILABLE INPUTS CURRENT · NOT AN ALL-CLEAR": "ENTRÉES DISPONIBLES À JOUR · PAS UN FEU VERT", + "SQUAWK {code} · {status}": "SQUAWK {code} · {status}", + "ADS-B PRIORITY · {status}": "PRIORITÉ ADS-B · {status}", + "ADS-B EMERGENCY · {status}": "URGENCE ADS-B · {status}", + "AIRCRAFT": "AÉRONEF", + "ACQUIRING SURFACE": "ACQUISITION SURFACE", + "SURFACE FALLBACK": "SECOURS SURFACE", + "STALE FEED": "FLUX OBSOLÈTE", + "LIVE TRACK": "PISTE EN DIRECT", + "COMMERCIAL": "COMMERCIAL", + "{aircraftClass} · {emergency} · {feedState}": "{aircraftClass} · {emergency} · {feedState}", + "{aircraftClass} · {feedState} · COURSE ALIGNED": "{aircraftClass} · {feedState} · ALIGNÉ SUR LA ROUTE", + "ARROW · ESTIMATED DIRECTION": "FLÈCHE · DIRECTION ESTIMÉE", + "ROUTE DATA UNAVAILABLE": "DONNÉES DE ROUTE INDISPONIBLES", + "DEST {bearing}": "DEST {bearing}", + "Disable cockpit weather effects": "Désactiver les effets météo du cockpit", + "Enable cockpit weather effects": "Activer les effets météo du cockpit", + "ON": "ON", + "Normal": "Normal", + "Night vision": "Vision nocturne", + "Thermal": "Thermique", + "Anime": "Anime", + "Noir": "Noir", + "Current cockpit vision style: {style}. Activate for next style.": "Style de vision actuel du cockpit : {style}. Activer pour le style suivant.", + "Current style: {style} — click for next": "Style actuel : {style} — cliquer pour le suivant", + "Collapse Contact panel": "Réduire le panneau Contact", + "Expand Contact panel": "Développer le panneau Contact", + "Collapse contact panel": "Réduire le panneau contact", + "Expand contact panel": "Développer le panneau contact", + "Collapse cockpit briefing panel": "Réduire le panneau de briefing du cockpit", + "Expand cockpit briefing panel": "Développer le panneau de briefing du cockpit", + "Collapse briefing panel": "Réduire le panneau de briefing", + "Expand briefing panel": "Développer le panneau de briefing", + "OBSERVED / MAPPED PINGS": "PINGS OBSERVÉS / CARTOGRAPHIÉS", + "SOURCE-BACKED EVENTS · NO SYNTHETIC NEWS": "ÉVÉNEMENTS SOURCÉS · AUCUNE ACTU SYNTHÉTIQUE", + "LATEST LOCATION-MATCHED REPORTING": "DERNIÈRES ACTUS LIÉES AU LIEU", + "{source} · LOCATION QUERY · RECENT": "{source} · REQUÊTE DE LIEU · RÉCENT", + "LOCAL INFO": "INFOS LOCALES", + "PLACE / CONDITIONS / POSITION": "LIEU / CONDITIONS / POSITION", + "TIME UNKNOWN": "HEURE INCONNUE", + "{count}M AGO": "IL Y A {count} MIN", + "{count}H AGO": "IL Y A {count} H", + "{count}D AGO": "IL Y A {count} J", + "DIR UNKNOWN": "DIR INCONNUE", + "Select flight {title}": "Sélectionner le vol {title}", + "{aircraftClass} · CURRENT": "{aircraftClass} · ACTUEL", + "MILITARY FLIGHT": "VOL MILITAIRE", + "COMMERCIAL FLIGHT": "VOL COMMERCIAL", + "{aircraftClass} · {distance}": "{aircraftClass} · {distance}", + "DISTANCE UNKNOWN": "DISTANCE INCONNUE", + "{count} INPUTS UNKNOWN": { + "one": "{count} ENTRÉE INCONNUE", + "many": "{count} D'ENTRÉES INCONNUES", + "other": "{count} ENTRÉES INCONNUES" + }, + "SOURCE STATUS UNAVAILABLE": "ÉTAT DE LA SOURCE INDISPONIBLE", + "Restore real aircraft": "Restaurer l'aéronef réel", + "Reclassify as TR-3B": "Reclasser en TR-3B", + "TRACK ACQUIRED": "PISTE ACQUISE", + "{label} · COURSE {heading}°": "{label} · ROUTE {heading}°", + "Context could not restore every layer; try again": "Le contexte n'a pas pu restaurer toutes les couches ; réessayez", + "No selected data layers": "Aucune couche de données sélectionnée", + "{count} data layers could not be cleared": { + "one": "{count} couche de données n'a pas pu être effacée", + "many": "{count} de couches de données n'ont pas pu être effacées", + "other": "{count} couches de données n'ont pas pu être effacées" + }, + "Cleared {count} data layers": { + "one": "{count} couche de données effacée", + "many": "{count} de couches de données effacées", + "other": "{count} couches de données effacées" + }, + "Selected data layers could not be cleared": "Impossible d'effacer les couches de données sélectionnées", + "Space Missions could not complete the requested transition; try again": "Les missions spatiales n'ont pas pu effectuer la transition demandée ; réessayez", + "Nearby installations refreshed": "Installations proches actualisées", + "Nearby installations could not be refreshed; try again": "Impossible d'actualiser les installations proches ; réessayez", + "Space Missions cancellation could not restore the previous layer state": "L'annulation des missions spatiales n'a pas pu restaurer l'état précédent des couches", + "That layer is unavailable in the current Context mode": "Cette couche est indisponible dans le mode Contexte actuel", + "{layerId} could not {action} cleanly": "{layerId} n'a pas pu {action} proprement", + "start": "démarrer", + "stop": "s'arrêter", + "{mode} could not start because another layer did not stop cleanly": "Impossible de démarrer {mode} : une autre couche ne s'est pas arrêtée proprement", + "Space Missions": "Missions spatiales", + "Context": "Contexte", + "Rendered globe frames per second · toggle with `": "Images du globe rendues par seconde · basculer avec `", + "imported map files": "les fichiers importés sur la carte", + "Ground heights could not be refreshed. Try the file again when the map has loaded.": "Impossible d'actualiser les altitudes du sol. Réessayez avec le fichier une fois la carte chargée.", + "{count} map objects": { + "one": "{count} objet cartographique", + "many": "{count} d'objets cartographiques", + "other": "{count} objets cartographiques" + }, + ", drawn with its own styles": ", dessiné avec ses propres styles", + "Hide {name}": "Masquer {name}", + "Show {name}": "Afficher {name}", + "Fly to {name}": "Rejoindre {name}", + "Remove {name}": "Retirer {name}", + "At most {count} files at once — remove one first.": "{count} fichiers au maximum à la fois — retirez-en un d'abord.", + "Reading {name}…": "Lecture de {name}…", + "That file could not be read.": "Impossible de lire ce fichier.", + "Added {count} files.": { + "one": "{count} fichier ajouté.", + "many": "{count} de fichiers ajoutés.", + "other": "{count} fichiers ajoutés." + }, + "{count} files could not be added ({name}: {error})": { + "one": "{count} fichier n'a pas pu être ajouté ({name} : {error})", + "many": "{count} de fichiers n'ont pas pu être ajoutés ({name} : {error})", + "other": "{count} fichiers n'ont pas pu être ajoutés ({name} : {error})" + }, + "Some KML content was left out.": "Une partie du contenu KML a été ignorée.", + "Press on the ground, not the sky": "Appuyez sur le sol, pas sur le ciel", + "Drag to size the box": "Faites glisser pour dimensionner la zone", + "light {source} to use your {name} key": "activez {source} pour utiliser votre clé {name}", + "light {source}": "activez {source}", + ", or ": ", ou ", + "{requirement}, or {offer}": "{requirement}, ou {offer}", + "Live Flights": "Vols en direct", + "Military Flights": "Vols militaires", + "Earthquakes (24h)": "Séismes (24 h)", + "Satellites": "Satellites", + "Space Missions (30d)": "Missions spatiales (30 j)", + "Street Traffic": "Trafic routier", + "CCTV": "CCTV", + "Radio": "Radio", + "Bikeshare": "Vélos partagés", + "Live AIS Vessels": "Navires AIS en direct", + "Mapped Installations": "Installations cartographiées", + "Global Context": "Contexte global", + "Datacenters": "Centres de données", + "Dams": "Barrages", + "Submarine Cables": "Câbles sous-marins", + "FIRMS Active Fires": "Feux actifs FIRMS", + "Local ADS-B": "ADS-B local", + "Transit": "Transports en commun", + "Recent Imagery": "Imagerie récente", + "Fire Perimeters": "Périmètres d'incendies", + "GNSS Integrity": "Intégrité GNSS", + "S2 GhostMaps": "S2 GhostMaps", + "Rain radar": "Radar de pluie", + "Lightning density": "Densité de foudre", + "Cyclone advisories": "Bulletins cycloniques", + "ALPR Cameras": "Caméras LAPI", + "Live Vessels": "Navires en direct", + "Bike Share": "Vélos partagés", + "Cameras": "Caméras", + "Street Level": "Niveau rue", + "Mapped ALPR Cameras": "Caméras LAPI cartographiées", + "Data Centers": "Centres de données", + "Active Fires": "Feux actifs", + "Military aircraft": "Aéronefs militaires", + "Vessels": "Navires", + "CCTV + Street View fallback": "CCTV + secours Street View", + "OpenStreetMap + optional Google Maps Places": "OpenStreetMap + Google Maps Places en option", + "GFS / ECMWF IFS · FORECAST": "GFS / ECMWF IFS · PRÉVISION", + "NOAA nowCOAST · OBSERVED": "NOAA nowCOAST · OBSERVÉ", + "NASA FIRMS · LIVE": "NASA FIRMS · DIRECT", + "OSM routing": "Itinéraires OSM", + "data source|Local": "Données locales", + "ENABLING": "ACTIV.", + "DISABLING": "DÉSACT.", + "Movement": "Mouvement", + "Infrastructure": "Infrastructures", + "Events": "Événements", + "Weather": "Météo", + "Utilities": "Outils", + "Other layers": "Autres couches", + "{state} · {source}": "{state} · {source}", + "UNCERTAIN · {source} · lifecycle state requires reconciliation": "INCERTAIN · {source} · état à rétablir", + "{state} · {source} · {detail} · retry {seconds}s": "{state} · {source} · {detail} · nouvel essai dans {seconds} s", + "{state} · {source} · {detail}": "{state} · {source} · {detail}", + "never": "jamais", + "{source} · {loadingLabel}": "{source} · {loadingLabel}", + "{accepted} of {raw} records accepted": "Enregistrements acceptés : {accepted} sur {raw}", + "incomplete snapshot": "instantané incomplet", + "{state} · {source} · {detail} · {ago}": "{state} · {source} · {detail} · {ago}", + "{state} · {source} · {ago} · retrying in {seconds}s": "{state} · {source} · {ago} · nouvel essai dans {seconds} s", + "{state} · {source} · {ago}": "{state} · {source} · {ago}", + "{source} · {ago}": "{source} · {ago}", + "UNCERTAIN": "INCERTAIN", + "status chip|UNAVAILABLE": "INDISP.", + "Data source unavailable": "Source de données indisponible", + "{name}: {state}": "{name} : {state}", + "{count}s ago": "il y a {count} s", + "Local FM stopped: internet radio started": "FM locale arrêtée : la radio Internet a démarré", + "IDLE": "INACTIF", + "CONNECTING": "CONNEXION", + "TUNING": "RÉGLAGE", + "STREAMING": "RÉCEPTION", + "UNSUPPORTED": "NON PRIS EN CHARGE", + "idle": "inactif", + "running": "actif", + "suspended": "suspendu", + "closed": "fermé", + "feed status|live": "en direct", + "feed status|stale": "obsolète", + "feed status|unreachable": "injoignable", + "feed status|invalid": "non valide", + "waiting for IQ": "en attente des données IQ", + "{count} messages": { + "one": "{count} message", + "many": "{count} de messages", + "other": "{count} messages" + }, + "DSP {count} blocks": { + "one": "DSP {count} bloc", + "many": "DSP {count} de blocs", + "other": "DSP {count} blocs" + }, + "DSP waiting": "DSP en attente", + "{level} dBFS audio": "audio {level} dBFS", + "audio signal --": "signal audio --", + "audio {state}": "audio {state}", + "{count} HEARD": { + "one": "CAPTÉS : {count}", + "many": "CAPTÉS : {count}", + "other": "CAPTÉS : {count}" + }, + "DISCONNECT": "DÉCONNECTER", + "CONNECT": "CONNECTER", + "LOCATING…": "LOCALISATION…", + "LOCATED": "LOCALISÉ", + "LOCATE": "LOCALISER", + " · read while Local ADS-B is on": " · lus tant qu'ADS-B local est activé", + "Decoder feeds: {feeds}": "Flux du décodeur : {feeds}", + "Language: {language}": "Langue : {language}", + "Change the interface language": "Changer la langue de l'interface", + "Interface language": "Langue de l'interface", + "Search languages": "Rechercher une langue", + "No matching languages": "Aucune langue correspondante", + "📍 Location: --": "📍 Lieu : --", + "Landmark: --": "Repère : --", + "Searched location": "Lieu recherché", + "ORBIT": "ORBITE", + "Location not found": "Lieu introuvable", + "Check the spelling and press Enter to search again.": "Vérifiez l'orthographe et appuyez sur Enter pour relancer la recherche.", + "Search failed": "Échec de la recherche", + "Press Enter to try again.": "Appuyez sur Enter pour réessayer.", + "Detailed outline unavailable": "Contour détaillé indisponible", + "Fly to a POI first": "Rejoignez d'abord un point d'intérêt", + "Reset to full globe view": "Revenir à la vue du globe entier", + "Reset cockpit to full globe view": "Réinitialiser le cockpit sur la vue du globe entier", + "Resetting to full globe view": "Retour à la vue du globe entier", + "Resetting cockpit to full globe view": "Réinitialisation du cockpit sur la vue du globe entier", + "MAP": "CARTE", + "Layers": "Couches", + "Search": "Recherche", + "More": "Plus", + "Panels": "Panneaux", + "panel": "le panneau", + "Expand {name}": "Développer {name}", + "Collapse {name}": "Réduire {name}", + "Expand Radio": "Développer la Radio", + "Collapse Radio": "Réduire la Radio", + "Expand Radio section": "Développer la section Radio", + "Collapse Radio section": "Réduire la section Radio", + "Panel layout updated — positions reset to new defaults": "Disposition des panneaux mise à jour — positions réinitialisées", + "Austin": "Austin", + "San Francisco": "San Francisco", + "New York": "New York", + "Tokyo": "Tokyo", + "London": "Londres", + "Paris": "Paris", + "Dubai": "Dubaï", + "Washington DC": "Washington", + "Tallinn": "Tallinn", + "Los Angeles": "Los Angeles", + "Collapse Cockpit display options": "Réduire les options d'affichage du cockpit", + "Expand Cockpit display options": "Développer les options d'affichage du cockpit", + "Collapse Cockpit Radio controls": "Réduire les commandes Radio du cockpit", + "Expand Cockpit Radio controls": "Développer les commandes Radio du cockpit", + "CH {index} / {total}": "CANAL {index} / {total}", + "NO STATIONS": "AUCUNE STATION", + "NO STATION AVAILABLE": "AUCUNE STATION DISPONIBLE", + "{name}, station {index} of {total}": "{name}, station {index} sur {total}", + "No station available": "Aucune station disponible", + "OFF AIR": "HORS ANTENNE", + "STATION UNAVAILABLE": "STATION INDISPONIBLE", + "Station unavailable after directory refresh": "Station indisponible après l'actualisation de l'annuaire", + "Radio could not {action} cleanly": "La Radio n'a pas pu {action} proprement", + "SYNC": "SYNCHRO", + "RECONCILE": "RÉTABLIR", + "DISABLE": "DÉSACTIVER", + "ENABLE": "ACTIVER", + "Reconcile Radio — lifecycle uncertain": "Rétablir la Radio — état incertain", + "Disable Radio": "Désactiver la Radio", + "Enable Radio": "Activer la Radio", + "{category} ({count})": "{category} ({count})", + "DIRECTORY BAND": "BANDE DE L'ANNUAIRE", + "{category} BAND": "BANDE {category}", + "NO STATION SELECTED": "AUCUNE STATION SÉLECTIONNÉE", + "Directory metadata only": "Métadonnées de l'annuaire seulement", + "Loading station directory…": "Chargement de l'annuaire des stations…", + "Choose a globe marker or use next.": "Choisissez un marqueur sur le globe ou utilisez SUIV.", + "TAGS · {tags}": "TAGS · {tags}", + "Play": "Lire", + "PAUSE": "PAUSE", + "RESUME": "REPRENDRE", + "PLAY": "LIRE", + "selected": "sélectionnée", + "nearest": "la plus proche", + "{action} {target} radio station": "{action} : station radio {target}", + "SYNCING": "SYNCHRO", + "READY": "PRÊT", + "station": "station", + " · stale/degraded directory": " · annuaire obsolète/dégradé", + " · degraded directory": " · annuaire dégradé", + " · stale directory": " · annuaire obsolète", + " · outside current filter": " · hors du filtre actuel", + "Ready — playback starts only from your action": "Prêt — la lecture ne démarre qu'à votre demande", + "Radio off": "Radio désactivée", + "Connecting directly to broadcaster…": "Connexion directe au diffuseur…", + "Buffering broadcaster stream…": "Mise en mémoire tampon du flux du diffuseur…", + "Playing {station}": "Lecture de {station}", + "Paused {station}": "{station} en pause", + "Broadcaster stream unavailable": "Flux du diffuseur indisponible", + " · muted during voice interaction": " · coupé pendant l'interaction vocale", + " · restoring volume after voice": " · rétablissement du volume après la voix", + " · static indicates no broadcaster audio": " · le grésillement indique l'absence de flux du diffuseur", + " · tuning static until broadcaster starts": " · grésillement pendant le réglage, jusqu'au début de la diffusion", + "Station unavailable after directory refresh — choose another channel": "Station indisponible après l'actualisation de l'annuaire — choisissez un autre canal", + "Radio is enabling…": "Activation de la Radio…", + "Radio is disabling…": "Désactivation de la Radio…", + "Radio lifecycle is uncertain — use Enable or Disable to reconcile": "État de la Radio incertain — utilisez Activer ou Désactiver pour le rétablir", + "Clear reading": "Effacer le relevé", + "Earlier observation": "Observation précédente", + "Observed history": "Historique des observations", + "Later observation": "Observation suivante", + "Latest": "Dernière", + "Newest frame per product": "Image la plus récente par produit", + "Replay observed history": "Rejouer l'historique des observations", + "Imagery on Esri · Google 3D returns when cleared": "Sur Esri · EFFACER rétablit Google 3D", + "Point the camera at the ground to use the view": "Orientez la caméra vers le sol pour utiliser la vue", + "That point cannot anchor a box": "Ce point ne peut pas ancrer une zone", + "Imagery stops at ±{limit}° latitude": "L'imagerie s'arrête à ±{limit}° de latitude", + "{owner} is using the pointer — close it first": "Pointeur utilisé par : {owner}. Fermez d'abord cet outil", + "Box is {size} wide · limit {limit}": "La zone fait {size} de large · limite {limit}", + "View is {size} wide · limit {limit}": "La vue fait {size} de large · limite {limit}", + "VS BASEMAP": "VS CARTE", + "A / B": "A / B", + "Newest low-cloud day for this box · scene cloud, not box cloud": "Jour peu nuageux le plus récent pour cette zone · nuages de la scène, pas de la zone", + "Newest day covering this box · cloudier than 20%": "Jour le plus récent couvrant cette zone · plus de 20 % de nuages", + "Newest day with imagery for this box · partial coverage": "Jour le plus récent avec des images pour cette zone · couverture partielle", + "Newest daily overview for this box": "Vue quotidienne la plus récente pour cette zone", + "newest clear day": "jour dégagé le plus récent", + "newest day (cloudy)": "jour le plus récent (nuageux)", + "newest day (partial coverage)": "jour le plus récent (couverture partielle)", + "newest overview": "vue quotidienne la plus récente", + "No imagery": "Aucune image", + "Loading": "Chargement", + "coverage|full": "complète", + "coverage|partial": "partielle", + "unknown": "inconnue", + "no imagery": "aucune image", + "{percent}% cloud": "{percent} % de nuages", + "{min}–{max}% cloud": "{min}–{max} % de nuages", + "NO BOX": "AUCUNE ZONE", + "SEARCHING": "RECHERCHE", + "{count} DAYS": { + "one": "{count} JOUR", + "many": "{count} DE JOURS", + "other": "{count} JOURS" + }, + "Zoom in or draw a smaller box": "Zoomez ou tracez une zone plus petite", + "Select a box or use the view": "Sélectionnez une zone ou utilisez la vue", + "Searching the last 30 days": "Recherche sur les 30 derniers jours", + "No days to show for this box": "Aucun jour à afficher pour cette zone", + "Drag the divider · SWAP trades sides": "Glissez le séparateur · INVERSER permute", + "← → preview · A or B pins the focused day": "← → aperçu · A ou B épingle le jour ciblé", + "← → preview the other side · A or B pins it": "← → aperçu de l'autre côté · A ou B l'épingle", + "S on another day replaces it · × unpins": "S sur un autre jour le remplace · × le désépingle", + "← → preview · S shows the focused day": "← → aperçu · S affiche le jour ciblé", + "ZOOM IN": "ZOOMER", + "Fly in until the view fits the 1,000 km limit": "Rapprochez-vous jusqu'à ce que la vue tienne dans la limite de 1 000 km", + "Imagery days, newest first · arrows move, S or A and B pin": "Jours d'imagerie, du plus récent au plus ancien · les flèches déplacent, S ou A et B épinglent", + "On the map": "Sur la carte", + "Mode": "Mode", + "Imagery opacity": "Opacité de l'imagerie", + "SWAP": "INVERSER", + "Trade the two sides of the divider": "Échanger les deux côtés du séparateur", + "SELECT BOX": "TRACER ZONE", + "Drag a box on the map (Esc cancels)": "Tracez une zone sur la carte (Esc pour annuler)", + "USE VIEW": "CETTE VUE", + "Use the current view as the box": "Utiliser la vue actuelle comme zone", + "Forget the box and its images": "Oublier la zone et ses images", + "START HERE": "COMMENCER ICI", + "Checking": "Vérification", + "PREVIEW": "APERÇU", + "Newest day with imagery for this box": "Jour le plus récent avec des images pour cette zone", + "SHOW": "AFFICHER", + "Pin {day} as {slot}": "Épingler {day} en {slot}", + "Show {day}": "Afficher {day}", + "pinned {slot}": "épinglé en {slot}", + "shown": "affiché", + "preview": "aperçu", + "start here": "commencer ici", + "No box": "Aucune zone", + "Searching": "Recherche", + "Every day is empty here": "Aucune image trouvée ici", + "Sources off": "Sources désactivées", + "VS": "VS", + "Not set": "Non défini", + "Basemap": "Fond de carte", + "Source off": "Source désactivée", + "Unpin {slot}": "Désépingler {slot}", + "Unpin the image": "Désépingler l'image", + "EXPORT {slot}": "EXP. {slot}", + "EXPORT": "EXPORTER", + "Download {slot} as a PNG": "Télécharger {slot} en PNG", + "Download the image as a PNG": "Télécharger l'image en PNG", + "Box {width} × {height} km": "Zone de {width} × {height} km", + "Acquired {time}": "Acquisition : {time}", + "Coverage {coverage}": "Couverture {coverage}", + "START HERE · {reason}": "COMMENCER ICI · {reason}", + "Daily overview shows little detail in a box this small": "La vue quotidienne montre peu de détails dans une zone aussi petite", + "Imagery: NASA GIBS and Worldview · HLS (Sentinel-2, Landsat 8/9) and VIIRS": "Imagerie : NASA GIBS et Worldview · HLS (Sentinel‑2, Landsat 8/9) et VIIRS", + "Details": "Détails", + "Times, coverage, sources": "Heures, couverture, sources", + "HIDE EMPTY DAYS": "MASQUER LES JOURS VIDES", + "SHOW EMPTY DAYS · {count}": "AFFICHER LES JOURS VIDES · {count}", + "Days the probe found empty in this box": "Jours sans image trouvée dans cette zone", + "image": "image", + "BASEMAP": "FOND DE CARTE", + "The basemap": "Le fond de carte", + "basemap": "fond de carte", + "Recent imagery against the basemap divider": "Séparateur imagerie récente / fond de carte", + "Recent imagery A and B divider": "Séparateur A / B de l'imagerie récente", + "{before} {left} percent, {after} {right} percent": "{before} {left} pour cent, {after} {right} pour cent", + "Export failed · {error}": "Échec de l'exportation · {error}", + "Scene action failed": "Échec de l'action sur la scène", + "New scene name": "Nom de la nouvelle scène", + "Delete scene \"{scene}\" and all shots?": "Supprimer la scène « {scene} » et tous ses plans ?", + "Delete shot \"{shot}\"?": "Supprimer le plan « {shot} » ?", + "Stopped (Esc)": "Arrêté (Esc)", + "SPARSE": "ÉPARS", + "BALANCED": "ÉQUILIBRÉ", + "No shots yet. Use CAPTURE SHOT to save current look.": "Aucun plan pour l'instant. Utilisez CAPTURER UN PLAN pour enregistrer le rendu actuel.", + "Double-click to rename": "Double-cliquez pour renommer", + "Shot name": "Nom du plan", + "LOAD": "CHARGER", + "DEL": "SUPPR.", + "{duration}s + {hold}s": "{duration} s + {hold} s", + "Choose files": "Choisir des fichiers", + "{count} files selected": { + "one": "{count} fichier sélectionné", + "many": "{count} de fichiers sélectionnés", + "other": "{count} fichiers sélectionnés" + }, + "No file chosen": "Aucun fichier choisi", + "Cancel": "Annuler", + "EDIT DETAILS": "MODIFIER LES DÉTAILS", + "SHARE SCENE": "PARTAGER LA SCÈNE", + "Shared flight follow expired": "Le suivi du vol partagé a expiré", + "Shared military flight follow expired": "Le suivi du vol militaire partagé a expiré", + "Shared satellite follow expired": "Le suivi du satellite partagé a expiré", + "Shared flight could not be restored — feed unavailable": "Impossible de restaurer le vol partagé — flux indisponible", + "Shared military flight could not be restored — feed unavailable": "Impossible de restaurer le vol militaire partagé — flux indisponible", + "Shared satellite could not be restored — feed unavailable": "Impossible de restaurer le satellite partagé — flux indisponible", + "Shared flight is unavailable": "Le vol partagé est indisponible", + "Shared military flight is unavailable": "Le vol militaire partagé est indisponible", + "Shared satellite is unavailable": "Le satellite partagé est indisponible", + "entity": "entité", + "Shared {subject} follow expired": "Suivi partagé expiré : {subject}", + "Shared {subject} could not be restored — feed unavailable": "Suivi partagé non restauré : {subject} — flux indisponible", + "Shared {subject} is unavailable": "Partage indisponible : {subject}", + "{id} isn't reporting now — showing where it was last seen": "{id} n'émet pas actuellement — affichage de sa dernière position connue", + "{id} isn't reporting now — showing where it was at {time}": "{id} n'émet pas actuellement — affichage de sa position à {time}", + "This share link can't be opened": "Ce lien de partage ne peut pas être ouvert", + "Shared vessel is unavailable": "Le navire partagé est indisponible", + "ACQUIRING": "ACQUISITION", + "SHARED {subject}": "PARTAGÉ : {subject}", + "subject": "cible", + "LOADING LIVE DATA": "CHARGEMENT DES DONNÉES EN DIRECT", + "LOAD COMPLETE": "CHARGEMENT TERMINÉ", + "LOAD CANCELLED": "CHARGEMENT ANNULÉ", + "LOAD FAILED": "ÉCHEC DU CHARGEMENT", + "LIVE DATA OFF": "DONNÉES EN DIRECT DÉSACTIVÉES", + "MAPPED SITES LOADED": "SITES CARTOGRAPHIÉS CHARGÉS", + "RETRYING MAPPED SITES": "NOUVEL ESSAI : SITES CARTOGRAPHIÉS", + "FETCHING MAPPED SITES": "RÉCUPÉRATION DES SITES CARTOGRAPHIÉS", + "RETRYING ALPR CAMERAS": "NOUVEL ESSAI : CAMÉRAS LAPI", + "FETCHING ALPR CAMERAS": "RÉCUPÉRATION DES CAMÉRAS LAPI", + "OVERPASS TEMPORARILY UNAVAILABLE": "OVERPASS TEMPORAIREMENT INDISPONIBLE", + "OVERPASS RATE-LIMITED": "OVERPASS : LIMITE DE REQUÊTES ATTEINTE", + "OVERPASS TIMED OUT": "OVERPASS : DÉLAI DÉPASSÉ", + "OVERPASS COULD NOT COMPLETE THE QUERY": "OVERPASS N'A PAS PU TERMINER LA REQUÊTE", + "MAP TILES TEMPORARILY UNAVAILABLE": "TUILES DE LA CARTE TEMPORAIREMENT INDISPONIBLES", + "MAPPED NAMES TEMPORARILY UNAVAILABLE": "NOMS CARTOGRAPHIÉS TEMPORAIREMENT INDISPONIBLES", + "TURNING OFF LIVE DATA": "DÉSACTIVATION DES DONNÉES EN DIRECT", + "REFRESHING LIVE DATA": "ACTUALISATION DES DONNÉES EN DIRECT", + "syncing road network": "synchro du réseau routier", + "retry pending": "nouvel essai en attente", + "retrying in {seconds}s": "nouvel essai dans {seconds} s", + "ALPR cameras": "caméras LAPI", + "OpenSky OAuth client missing/invalid": "Client OAuth OpenSky absent ou non valide", + "OpenSky OAuth rejected credentials": "OpenSky OAuth a refusé les identifiants", + "OpenSky username/password rejected": "Identifiant ou mot de passe OpenSky refusé", + "OpenSky auth missing": "Authentification OpenSky absente", + "OpenSky auth required": "Authentification OpenSky requise", + "OpenSky auth invalid": "Authentification OpenSky non valide", + "OpenSky auth failed": "Échec de l'authentification OpenSky", + "KartaView answered HTTP {status}": "KartaView a répondu HTTP {status}", + "Panoramax answered HTTP {status}": "Panoramax a répondu HTTP {status}", + "KartaView refused the request (code {code})": "KartaView a refusé la requête (code {code})", + "Vector tiles unavailable (HTTP {status})": "Tuiles vectorielles indisponibles (HTTP {status})", + "GNSS feed unavailable (HTTP {status})": "Flux GNSS indisponible (HTTP {status})", + "{count} CelesTrak groups unavailable": { + "one": "{count} groupe CelesTrak indisponible", + "many": "{count} de groupes CelesTrak indisponibles", + "other": "{count} groupes CelesTrak indisponibles" + }, + "{feed} feed unavailable": "Flux {feed} indisponible", + "Source snapshot {minutes} min old": "Instantané de la source datant de {minutes} min", + "{source} rate limited (anonymous)": "{source} : limite de requêtes atteinte (anonyme)", + "{source} rate limited": "{source} : limite de requêtes atteinte", + "{source} network error": "{source} : erreur réseau", + "Malformed {source} aircraft rows": "{source} : lignes d'aéronefs mal formées", + "Malformed {source} response": "{source} : réponse mal formée", + "entry {n}": "entrée {n}", + "feeds {names} stale": { + "one": "flux {names} obsolète", + "many": "flux {names} obsolètes", + "other": "flux {names} obsolètes" + }, + "feeds {names} unreachable": { + "one": "flux {names} injoignable", + "many": "flux {names} injoignables", + "other": "flux {names} injoignables" + }, + "feeds {names} invalid": { + "one": "flux {names} non valide", + "many": "flux {names} non valides", + "other": "flux {names} non valides" + }, + "Loading {feeds}": "Chargement de {feeds}", + "{count} heard": { + "one": "{count} aéronef capté", + "many": "{count} d'aéronefs captés", + "other": "{count} aéronefs captés" + }, + "USB {rate} msg/s": "USB {rate} msg/s", + "{rate} msg/s": "{rate} msg/s", + "{count} feeds live": { + "one": "{count} flux en direct", + "many": "{count} de flux en direct", + "other": "{count} flux en direct" + }, + "{feed} {count} ({seconds}s old)": "{feed} {count} (il y a {seconds} s)", + "Street Level toggle failed": "Impossible d'activer ou de désactiver Niveau rue", + "Street Level could not start": "Impossible de démarrer Niveau rue", + "SHRINK": "RÉDUIRE", + "EXPAND": "AGRANDIR", + "Shrink": "Réduire", + "Expand": "Agrandir", + "ANY DATE": "TOUTES DATES", + "LAST 10 YEARS": "10 DERNIÈRES ANNÉES", + "LAST 5 YEARS": "5 DERNIÈRES ANNÉES", + "LAST 3 YEARS": "3 DERNIÈRES ANNÉES", + "LAST 2 YEARS": "2 DERNIÈRES ANNÉES", + "LAST YEAR": "12 DERNIERS MOIS", + "LAST 6 MONTHS": "6 DERNIERS MOIS", + "LAST 3 MONTHS": "3 DERNIERS MOIS", + "LAST MONTH": "30 DERNIERS JOURS", + "LAST {count} DAYS": { + "one": "{count} DERNIER JOUR", + "many": "{count} DERNIERS JOURS", + "other": "{count} DERNIERS JOURS" + }, + "{window} · SINCE {date}": "{window} · DEPUIS LE {date}", + "Turn Street Level off": "Désactiver Niveau rue", + "Turn Street Level on": "Activer Niveau rue", + "{name} imagery on": "Images {name} activées", + "{name} imagery off": "Images {name} désactivées", + "Image by {creator}": "Image de {creator}", + " or ": " ou ", + "OPEN {labels}": "OUVRIR {labels}", + "Open {names} at the centre of the view": "Ouvrir {names} au centre de la vue", + "Zoom in to a street to open {names} here": "Zoomez sur une rue pour ouvrir {names} ici", + "Switch Street Level on to draw its coverage.": "Activez Niveau rue pour afficher sa couverture.", + "Every provider is switched off: light a chip to draw its coverage.": "Tous les fournisseurs sont désactivés : activez-en un pour afficher sa couverture.", + "Loading this sequence…": "Chargement de cette séquence…", + "{n} images in this sequence · Esc clears": { + "one": "{n} image dans cette séquence · Esc pour effacer", + "many": "{n} d'images dans cette séquence · Esc pour effacer", + "other": "{n} images dans cette séquence · Esc pour effacer" + }, + "No sequences in view match the filter": "Aucune séquence dans la vue ne correspond au filtre", + "No coverage in view": "Aucune couverture dans la vue", + "{n} streets in view · click a line for its nearest photo": { + "one": "{n} rue dans la vue · cliquez sur une ligne pour sa photo la plus proche", + "many": "{n} de rues dans la vue · cliquez sur une ligne pour sa photo la plus proche", + "other": "{n} rues dans la vue · cliquez sur une ligne pour sa photo la plus proche" + }, + "{n} sequences in view · click a line for its nearest photo": { + "one": "{n} séquence dans la vue · cliquez sur une ligne pour sa photo la plus proche", + "many": "{n} de séquences dans la vue · cliquez sur une ligne pour sa photo la plus proche", + "other": "{n} séquences dans la vue · cliquez sur une ligne pour sa photo la plus proche" + }, + "{n} sequences in view · click a line for its photos": { + "one": "{n} séquence dans la vue · cliquez sur une ligne pour voir ses photos", + "many": "{n} de séquences dans la vue · cliquez sur une ligne pour voir ses photos", + "other": "{n} séquences dans la vue · cliquez sur une ligne pour voir ses photos" + }, + "km/h": "km/h", + "m/s": "m/s", + "mph": "mph", + "hPa": "hPa", + "kt": "nd", + "kts": "nd", + "ft": "ft", + "km": "km", + "m": "m", + "h": "h", + "min": "min", + "s": "s", + "dB": "dB", + "Celestial ring — reveal the full globe": "Anneau céleste — révéler le globe entier", + "Celestial ring — available in Normal style": "Anneau céleste — disponible en style Normal", + "Detection overlay: {mode}": "Surcouche de détection : {mode}", + "Detection overlay: off": "Surcouche de détection : désactivée", + "DETECT": "DÉTECT.", + "{time} · {minutes} min ago": "{time} · il y a {minutes} min", + "Active weather": "Couches météo actives", + "LATEST · newest per product": "DERNIÈRE · la plus récente par produit", + "Forecast · valid {valid} · issued {issued}": "Prévision · valide {valid} · émise {issued}", + "{detail} · Does not follow history": "{detail} · Ne suit pas l'historique", + "Forecast · valid {valid}": "Prévision · valide {valid}", + "voice|OFF": "OFF", + "voice|CONNECTING": "CONNEXION", + "voice|LISTENING": "ÉCOUTE", + "voice|EXECUTING": "EXÉCUTION", + "voice|ERROR": "ERREUR", + "Voice provider": "Fournisseur vocal", + "Changing provider reconnects voice if it is on.": "Changer de fournisseur reconnecte la voix si elle est activée.", + "Voice Settings": "Paramètres vocaux", + "AI AGENT": "AGENT IA", + "Voice model tier — applies next session": "Niveau du modèle vocal — à la prochaine session", + "STD": "STD", + "Estimated session cost": "Coût estimé de la session", + "Voice control — activate to toggle voice; hold Space to speak": "Commande vocale — activer/désactiver ; maintenez Space pour parler", + "ON/OFF": "ON/OFF", + "VOICE STANDBY": "VOIX EN VEILLE", + "VOICE CONTROL": "COMMANDE VOCALE", + "Hold Space to speak · tap Space to activate focused controls": "Maintenez Space pour parler · appuyez sur Space pour activer la commande ciblée", + "VOICE": "VOIX", + "Preview": "Aperçu", + "Settings": "Paramètres", + "Close voice panel": "Fermer le panneau vocal", + "Voice replies appear here.": "Les réponses vocales s'affichent ici.", + "Voice assistant": "Assistant vocal", + "Dismiss voice card": "Fermer la fiche vocale", + "YOU": "VOUS", + "THIS": "CECI", + "Plan": "Plan", + "Referenced items": "Éléments cités", + "NOTES & SOURCES": "NOTES ET SOURCES", + "Show Voice Preview": "Afficher l'aperçu vocal", + "Voice inactivity timeout": "Délai d'inactivité vocale", + "Minutes": "Minutes", + "Custom": "Personnalisé", + "These preferences stay in this browser.": "Ces préférences restent dans ce navigateur.", + "VOICE SYSTEM ERROR": "ERREUR DU SYSTÈME VOCAL", + "DISMISS": "FERMER", + "Check microphone permission and network access, then try again.": "Vérifiez l'autorisation du microphone et l'accès réseau, puis réessayez.", + "Gemini returned an oversized audio chunk": "Gemini a renvoyé un fragment audio trop volumineux", + "Gemini returned an unsupported audio format": "Gemini a renvoyé un format audio non pris en charge", + "Gemini returned an invalid audio sample rate": "Gemini a renvoyé une fréquence d'échantillonnage audio non valide", + "Gemini returned incomplete PCM audio": "Gemini a renvoyé un audio PCM incomplet", + "This browser does not support Gemini microphone audio": "Ce navigateur ne prend pas en charge l'audio du microphone pour Gemini", + "Microphone audio processing failed": "Échec du traitement de l'audio du microphone", + "Gemini audio playback fell behind. Reconnect to continue.": "La lecture audio de Gemini a pris du retard. Reconnectez-vous pour continuer.", + "Gemini token response did not include an ephemeral credential": "La réponse de Gemini ne contenait pas d'identifiant temporaire", + "Invalid Gemini connection endpoint": "Point de connexion Gemini non valide", + "Gemini voice could not connect. Try again.": "Impossible de connecter la voix Gemini. Réessayez.", + "Gemini voice requires a same-origin app request": "La voix Gemini exige une requête de l'application de même origine", + "Add a Gemini API key in Provider Settings": "Ajoutez une clé API Gemini dans les paramètres des fournisseurs", + "The Gemini Live model configuration is invalid": "La configuration du modèle Gemini Live n'est pas valide", + "Gemini voice is starting too often. Try again shortly.": "La voix Gemini démarre trop souvent. Réessayez dans un instant.", + "Gemini quota is unavailable. Try again later.": "Le quota Gemini est indisponible. Réessayez plus tard.", + "Gemini access was refused. Check the API key and model access.": "L'accès à Gemini a été refusé. Vérifiez la clé API et l'accès au modèle.", + "Gemini Live is unavailable for the configured model.": "Gemini Live n'est pas disponible pour le modèle configuré.", + "Token request is too large": "La demande de jeton est trop volumineuse", + "Use an empty JSON token request": "Utilisez une demande de jeton avec un objet JSON vide", + "Release Space to send": "Relâchez Space pour envoyer", + "Hold Space to talk": "Maintenez Space pour parler", + "Voice command context is too large": "Le contexte de la commande vocale est trop volumineux", + "Gemini reliable transport exceeded its queue limit. Start voice again.": "La file d'envoi fiable de Gemini a dépassé sa limite. Relancez la voix.", + "Gemini reliable transport did not drain. Start voice again.": "La file d'envoi fiable de Gemini ne s'est pas vidée. Relancez la voix.", + "Gemini token request failed (HTTP {status})": "Échec de la demande de jeton Gemini (HTTP {status})", + "Gemini token response did not include a valid model": "La réponse de Gemini ne contenait pas de modèle valide", + "Gemini token response did not include session settings": "La réponse de Gemini ne contenait pas les paramètres de session", + "Gemini session settings do not match the input mode": "Les paramètres de session Gemini ne correspondent pas au mode de saisie", + "Requesting microphone": "Demande d'accès au microphone", + "Gemini could not submit this input. Start voice again.": "Gemini n'a pas pu envoyer cette saisie. Relancez la voix.", + "Connecting to Gemini": "Connexion à Gemini", + "Ask or command": "Question ou commande", + "Gemini returned an oversized message": "Gemini a renvoyé un message trop volumineux", + "Gemini Live connection failed. Check network access, then start voice again.": "Échec de la connexion à Gemini Live. Vérifiez l'accès au réseau, puis relancez la voix.", + "Gemini Live connection ended. Start voice again for a new conversation.": "La connexion à Gemini Live est terminée. Relancez la voix pour une nouvelle conversation.", + "Gemini Live could not start. Start voice again to request a fresh session.": "Gemini Live n'a pas pu démarrer. Relancez la voix pour demander une nouvelle session.", + "Start voice again for a fresh token and a new conversation.": "Relancez la voix pour obtenir un nouveau jeton et démarrer une nouvelle conversation.", + "Start voice again to request a fresh session credential.": "Relancez la voix pour demander un nouvel identifiant de session.", + "Microphone permission was denied. Allow microphone access and try again.": "L'autorisation du microphone a été refusée. Autorisez l'accès au microphone et réessayez.", + "Gemini connection timed out. Try again.": "Délai de connexion à Gemini dépassé. Réessayez.", + "Gemini voice could not start": "Impossible de démarrer la voix Gemini", + "Gemini did not finish the interrupted reply. Start voice again.": "Gemini n'a pas terminé la réponse interrompue. Relancez la voix.", + "Gemini session expiring; finishing the current reply": "La session Gemini expire ; fin de la réponse en cours", + "Gemini rejected the voice session. Start voice again to request a fresh session.": "Gemini a refusé la session vocale. Relancez la voix pour demander une nouvelle session.", + "Start voice again for a fresh session credential. If it repeats, check Gemini model and API access.": "Relancez la voix pour obtenir un nouvel identifiant de session. Si cela se reproduit, vérifiez le modèle Gemini et l'accès à l'API.", + "Gemini action history is full. Start voice again for a fresh session.": "L'historique des actions de Gemini est plein. Relancez la voix pour une nouvelle session.", + "Gemini requested too many simultaneous actions": "Gemini a demandé trop d'actions simultanées", + "Running command": "Commande en cours", + "Voice text command is too long": "La commande écrite est trop longue", + "Gemini voice is not connected": "La voix Gemini n'est pas connectée", + "Gemini interrupted input exceeded its buffer or wait limit. Start voice again.": "La saisie interrompue de Gemini a dépassé sa limite de tampon ou d'attente. Relancez la voix.", + "Gemini Live connection is expiring. Start voice again for a new conversation.": "La connexion à Gemini Live expire. Relancez la voix pour une nouvelle conversation.", + "Voice off": "Voix désactivée", + "Gemini voice sessions have a time limit. When a session expires, start voice again for a new conversation.": "Les sessions vocales Gemini sont limitées dans le temps. Quand une session expire, relancez la voix pour une nouvelle conversation.", + "Stopped by voice": "Arrêté par la voix", + "Automatic stop is unavailable for this voice connection.": "L'arrêt automatique n'est pas disponible pour cette connexion vocale.", + "Preview is off.": "L'aperçu est désactivé.", + "Your choice applies now, but could not be saved.": "Votre choix s'applique maintenant, mais n'a pas pu être enregistré.", + "WebRTC microphone support unavailable": "Microphone WebRTC non pris en charge", + "VOICE UNAVAILABLE": "VOIX INDISPONIBLE", + "VOICE ACTIVE": "VOIX ACTIVE", + "Voice session could not be started.": "Impossible de démarrer la session vocale.", + "MINI": "MINI", + "Next session: {pendingId} — this session stays on {modelId}": "Prochaine session : {pendingId} — cette session reste sur {modelId}", + "Voice model: {pendingId} — click to switch to {target}; applies next session": "Modèle vocal : {pendingId} — cliquer pour passer à {target} ; à la prochaine session", + "standard": "standard", + "mini": "mini", + "COST UNKNOWN": "COÛT INCONNU", + "ChatGPT OAuth session: {responses} response(s), {input} input and {output} output tokens, {captions} caption transcription(s) reported.": "Session OAuth ChatGPT (données signalées) : {responses} réponse(s), {input} jetons d'entrée et {output} jetons de sortie, {captions} transcription(s) de sous-titres.", + "USD cost and the API spend cap are unavailable for this auth mode.": "Le coût en USD et le plafond de dépenses API ne sont pas disponibles avec ce mode d'authentification.", + "Usage is incomplete because a response was still in flight when the session ended.": "L'utilisation est incomplète, car une réponse était encore en cours à la fin de la session.", + "Estimated session cost on {modelId} — {responses} response(s). Warns at {warn}, ends the session at {cap}.": "Coût estimé de la session sur {modelId} — {responses} réponse(s). Alerte à {warn}, fin de session à {cap}.", + "Estimate is incomplete — a response was still in flight when the session ended, so its usage was never reported.": "L'estimation est incomplète — une réponse était encore en cours à la fin de la session, son utilisation n'a donc jamais été signalée.", + "STANDARD": "STANDARD", + "{tier} applies next session": "{tier} à la prochaine session", + "Session ended — cost cap {cost}": "Session terminée — plafond de coût {cost}", + "MIC": "MIC", + "Radio did not start": "La radio n'a pas démarré", + "{n} aircraft": { + "one": "{n} aéronef", + "many": "{n} d'aéronefs", + "other": "{n} aéronefs" + }, + "{n} military aircraft": { + "one": "{n} aéronef militaire", + "many": "{n} d'aéronefs militaires", + "other": "{n} aéronefs militaires" + }, + "{n} ships": { + "one": "{n} navire", + "many": "{n} de navires", + "other": "{n} navires" + }, + "{n} satellites": { + "one": "{n} satellite", + "many": "{n} de satellites", + "other": "{n} satellites" + }, + "{n} launches": { + "one": "{n} lancement", + "many": "{n} de lancements", + "other": "{n} lancements" + }, + "{n} earthquakes": { + "one": "{n} séisme", + "many": "{n} de séismes", + "other": "{n} séismes" + }, + "{n} fires": { + "one": "{n} feu", + "many": "{n} de feux", + "other": "{n} feux" + }, + "{n} fire perimeters": { + "one": "{n} périmètre d'incendie", + "many": "{n} de périmètres d'incendie", + "other": "{n} périmètres d'incendie" + }, + "{n} cyclones": { + "one": "{n} cyclone", + "many": "{n} de cyclones", + "other": "{n} cyclones" + }, + "{n} buses": { + "one": "{n} bus", + "many": "{n} de bus", + "other": "{n} bus" + }, + "{n} bike share": { + "one": "{n} station de vélos partagés", + "many": "{n} de stations de vélos partagés", + "other": "{n} stations de vélos partagés" + }, + "{n} flock cameras": { + "one": "{n} caméra LAPI", + "many": "{n} de caméras LAPI", + "other": "{n} caméras LAPI" + }, + "{n} military bases": { + "one": "{n} base militaire", + "many": "{n} de bases militaires", + "other": "{n} bases militaires" + }, + "{n} datacenters": { + "one": "{n} centre de données", + "many": "{n} de centres de données", + "other": "{n} centres de données" + }, + "{n} dams": { + "one": "{n} barrage", + "many": "{n} de barrages", + "other": "{n} barrages" + }, + "{n} air quality": { + "one": "{n} station de qualité de l'air", + "many": "{n} de stations de qualité de l'air", + "other": "{n} stations de qualité de l'air" + }, + "{n} results": { + "one": "{n} résultat", + "many": "{n} de résultats", + "other": "{n} résultats" + }, + "anywhere in the loaded data": "dans toutes les données chargées", + "in view": "dans la vue", + "within {radius} of the view centre": "dans un rayon de {radius} autour du centre de la vue", + "within {radius} of {place} (Contacts window)": "dans un rayon de {radius} autour de {place} (fenêtre Contacts)", + "within {radius} of {place}": "dans un rayon de {radius} autour de {place}", + "within {radius}": "dans un rayon de {radius}", + "over {region}": "au-dessus de {region}", + "At least {items}": "Au moins {items}", + "{items} {scope}": "{items} {scope}", + "last 24 h": "dernières 24 h", + "past 30 d and upcoming": "30 derniers jours et à venir", + "the flights layer loads by viewport": "la couche des vols se charge selon la vue", + "distance is ground distance, not overhead": "la distance est mesurée au sol jusqu'au point subsatellite, pas jusqu'au satellite", + "the count is a lower bound because at least one layer reached its retained limit.": "le décompte est un minimum, car au moins une couche a atteint sa limite de rétention.", + "{label} off": "Couche désactivée : {label}", + "{label} loading": "{label} : chargement", + "{label} error": "{label} : erreur", + "counted the first {returned} loaded records": "{returned} premiers enregistrements chargés comptés", + "counted {returned} of {total} loaded records": "{returned} enregistrements chargés comptés sur {total}", + "{layers} just turned on — still loading, counts will rise": "{layers} : activation récente — chargement en cours, les décomptes vont augmenter", + "Contacts panel snapshot at {time} — the same retained cohort supplies the card, speech, and follow-up memory": "Instantané du panneau Contacts ({time}) — le même groupe retenu alimente la fiche, la voix et la mémoire de suivi", + "Contacts panel snapshot — the same retained cohort supplies the card, speech, and follow-up memory": "Instantané du panneau Contacts — le même groupe retenu alimente la fiche, la voix et la mémoire de suivi", + "Fly below {km} km to a covered region": "Descendez sous {km} km au-dessus d'une région couverte", + "No feed here yet · {n} regions available": { + "one": "Pas encore de flux ici · {n} région disponible", + "many": "Pas encore de flux ici · {n} de régions disponibles", + "other": "Pas encore de flux ici · {n} régions disponibles" + }, + "degraded": "dégradé", + "stale": "obsolète", + "partial": "partiel", + "fallback": "secours", + "unavailable": "indisponible", + "Not answered: {layers}": "Sans réponse : {layers}", + "Toggle voice control": "Activer/désactiver la commande vocale", + "Activate to toggle voice": "Activer/désactiver la commande vocale", + "Voice active": "Voix active", + "Voice could not be started.": "Impossible de démarrer la voix.", + "the active voice session": "la session vocale active", + "{value} feet": "{value} pieds", + "Fire · FRP {frp} MW": "Feu · FRP {frp} MW", + "The flights layer is not enabled": "La couche des vols n'est pas activée", + "The military layer is not enabled": "La couche des vols militaires n'est pas activée", + "The satellites layer is not enabled": "La couche des satellites n'est pas activée", + "The ais-live-vessels layer is not enabled": "La couche des navires AIS n'est pas activée", + "Could not enable the requested layer": "Impossible d'activer la couche demandée", + "Could not disable the requested layer": "Impossible de désactiver la couche demandée", + "Radio layer unavailable": "Couche radio indisponible", + "Camera navigation is unavailable in the current view": "La navigation de la caméra n'est pas disponible dans la vue actuelle", + "Camera navigation policy unavailable": "Règle de navigation de la caméra indisponible", + "Nearest-aircraft selection supports Flights or Military Flights only": "La sélection de l'aéronef le plus proche ne fonctionne qu'avec Vols ou Vols militaires", + "Nearest-aircraft selection needs a preset, place name, or latitude and longitude": "La sélection de l'aéronef le plus proche nécessite un préréglage, un nom de lieu ou une latitude et une longitude", + "Could not arrive at the requested place": "Impossible d'atteindre le lieu demandé", + "The nearest airborne aircraft could not be selected": "Impossible de sélectionner l'aéronef en vol le plus proche", + "No flights within {radius} of the current view": "Aucun vol dans un rayon de {radius} autour de la vue actuelle", + "No military within {radius} of the current view": "Aucun vol militaire dans un rayon de {radius} autour de la vue actuelle", + "No satellites within {radius} of the current view": "Aucun satellite dans un rayon de {radius} autour de la vue actuelle", + "No vessels within {radius} of the current view": "Aucun navire dans un rayon de {radius} autour de la vue actuelle", + "{layer} is enabled on the {source} feed, but no airborne aircraft is loaded in the {place} view yet": "La couche {layer} est activée sur le flux {source}, mais aucun aéronef en vol n'est encore chargé dans la vue {place}", + "{layer} is enabled, but no airborne aircraft is loaded in the {place} view yet": "La couche {layer} est activée, mais aucun aéronef en vol n'est encore chargé dans la vue {place}", + "{layer} is enabled, but its aircraft feed is unavailable": "La couche {layer} est activée, mais son flux d'aéronefs est indisponible", + "{layer} is enabled, but its destination refresh did not complete": "La couche {layer} est activée, mais son actualisation à destination ne s'est pas terminée", + "{layer} is enabled, but {source} is unavailable": "La couche {layer} est activée, mais {source} est indisponible", + "Could not arrive at {place}": "Impossible d'atteindre {place}", + "{name} is no longer loaded": "{name} n'est plus chargé", + "Unknown target layer: {target}": "Couche cible inconnue : {target}", + "nominal": "nominal", + "{label} on": "Couche activée : {label}", + "Frame aircraft": "Cadrer les aéronefs", + "Frame military aircraft": "Cadrer les aéronefs militaires", + "Frame ships": "Cadrer les navires", + "Frame satellites": "Cadrer les satellites", + "Frame contacts": "Cadrer les contacts", + "{count} aircraft in frame": { + "one": "{count} aéronef dans le cadre", + "many": "{count} d'aéronefs dans le cadre", + "other": "{count} aéronefs dans le cadre" + }, + "{count} military aircraft in frame": { + "one": "{count} aéronef militaire dans le cadre", + "many": "{count} d'aéronefs militaires dans le cadre", + "other": "{count} aéronefs militaires dans le cadre" + }, + "{count} ships in frame": { + "one": "{count} navire dans le cadre", + "many": "{count} de navires dans le cadre", + "other": "{count} navires dans le cadre" + }, + "{count} satellites in frame": { + "one": "{count} satellite dans le cadre", + "many": "{count} de satellites dans le cadre", + "other": "{count} satellites dans le cadre" + }, + "{count} contacts in frame": { + "one": "{count} contact dans le cadre", + "many": "{count} de contacts dans le cadre", + "other": "{count} contacts dans le cadre" + }, + "Nearest military aircraft": "Aéronefs militaires les plus proches", + "Nearest ships": "Navires les plus proches", + "Nearest satellites": "Satellites les plus proches", + "Nearest aircraft": "Aéronefs les plus proches", + "{first} and {second}": "{first} et {second}", + "Layer": "Couche", + "there": "là", + "Camera range {range}": "Distance de la caméra {range}", + "Within {radius} of the view": "Dans un rayon de {radius} autour de la vue", + "Labels on": "Étiquettes activées", + "Framing uses the nearest 80 loaded contacts": "Le cadrage utilise les 80 contacts chargés les plus proches", + "{distance} from {location}": "À {distance} de {location}", + "Altitude {altitude}": "Altitude {altitude}", + "{origin} to {destination}": "De {origin} à {destination}", + "from {origin}": "depuis {origin}", + "to {destination}": "vers {destination}", + "Registration {registration}": "Immatriculation {registration}", + "View state": "État de la vue", + "Marked {count} places": { + "one": "{count} lieu marqué", + "many": "{count} de lieux marqués", + "other": "{count} lieux marqués" + }, + "Nothing marked": "Rien de marqué", + "Not found: {places}": "Introuvable : {places}", + "Tracing outlines": "Tracé des contours", + "Frame flights": "Cadrer les vols", + "Frame military": "Cadrer les vols militaires", + "Frame vessels": "Cadrer les navires", + "Frame {target}": "Cadrer {target}", + "Zoom out to globe": "Dézoomer jusqu'au globe", + "Open data layers": "Ouvrir les couches de données", + "Open or close a panel": "Ouvrir ou fermer un panneau", + "Change context mode": "Changer le mode de contexte", + "Control cockpit": "Piloter le cockpit", + "Change visual style": "Changer le style visuel", + "Adjust HUD": "Régler le HUD", + "Adjust cyber sonar": "Régler le sonar Cyber", + "Adjust detection overlay": "Régler la surcouche de détection", + "Change map stack": "Changer de fond de carte", + "Adjust post-processing": "Régler le post-traitement", + "Control scene": "Piloter la scène", + "Control CCTV": "Piloter les caméras CCTV", + "Control radio": "Piloter la radio", + "Track contact": "Suivre le contact", + "Stop tracking": "Arrêter le suivi", + "Clear marks": "Effacer les marques", + "Move camera": "Déplacer la caméra", + "Fly route": "Survoler l'itinéraire", + "Analyst query": "Requête d'analyse", + "Next ISS pass": "Prochain passage de l'ISS", + "Next satellite pass": "Prochain passage du satellite", + "Mark the map": "Marquer la carte", + "Mark {targets} +{more}": "Marquer : {targets} +{more}", + "Mark {targets}": "Marquer : {targets}", + "Fly to {place}": "Rejoindre {place}", + "Fly to Location": "Rejoindre le lieu", + "Nearest aircraft to {place}": "Aéronef le plus proche de {place}", + "Nearest aircraft to Here": "Aéronef le plus proche d'ici", + "Layer on": "Couche : activée", + "Layer off": "Couche : désactivée", + "Zoom out": "Dézoomer", + "Zoom in": "Zoomer", + "Read the view": "Lire la vue", + "Check view state": "Vérifier l'état de la vue", + "Action": "Action", + "Finding places": "Recherche de lieux", + "Tracing outline": "Tracé du contour", + "Looking up place": "Recherche du lieu", + "Flying": "En vol", + "Turning on layer": "Activation de la couche", + "Loading aircraft": "Chargement des aéronefs", + "Picking nearest": "Choix du plus proche", + "Working": "En cours", + "in progress": "en cours", + "done": "terminé", + "failed": "échec", + "cancelled": "annulé", + "Outline traced": "Contour tracé", + "Outline unavailable": "Contour indisponible", + "Source: {label}": "Source : {label}", + "WORKING": "EN COURS", + "DONE": "TERMINÉ", + "Recent speech": "Propos récents", + "HERE": "ICI", + "Aircraft cockpit view": "Vue cockpit de l'aéronef", + "LEVEL": "HORIZON", + "Estimated destination direction": "Direction estimée de la destination", + "DEST ---°": "DEST ---°", + "OPTICAL PLANE · 01": "PLAN OPTIQUE · 01", + "VISOR LOCK · ACTIVE": "VERROU VISIÈRE · ACTIF", + "GROUND SPEED · KTS": "VITESSE SOL · ND", + "ALTITUDE · FT": "ALTITUDE · FT", + "FIRST PERSON": "PREMIÈRE PERSONNE", + "LIVE TRACK · COURSE ALIGNED": "PISTE EN DIRECT · ALIGNÉ SUR LA ROUTE", + "Cockpit vision style": "Style de vision du cockpit", + "Previous cockpit vision style": "Style de vision précédent du cockpit", + "Previous vision style": "Style de vision précédent", + "Current cockpit vision style: NORMAL. Activate for next style.": "Style de vision actuel du cockpit : NORMAL. Activer pour le style suivant.", + "Current style: NORMAL — click for next": "Style actuel : NORMAL — cliquer pour le suivant", + "CURRENT": "ACTUEL", + "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", + "GROUND SPEED": "VITESSE SOL", + "KTS": "ND", + "ALTITUDE": "ALTITUDE", + "FT": "FT", + "Contact cockpit summary": "Résumé cockpit du contact", + "CONTACT": "CONTACT", + "CONTACTS · 250 KM": "CONTACTS · 250 KM", + "Contact navigation": "Navigation des contacts", + "CONTEXT ONLY": "CONTEXTE SEULEMENT", + "Nearby cohort counts": "Nombre de contacts par groupe à proximité", + "FLT": "VOLS", + "MIL": "MIL", + "SITE": "SITES", + "NEAREST OBSERVED / MAPPED": "PLUS PROCHE OBSERVÉ / CARTOGRAPHIÉ", + "AVAILABLE INPUTS ONLY · NOT AN ALL-CLEAR": "ENTRÉES DISPONIBLES SEULEMENT · PAS UN FEU VERT", + "WX": "MÉTÉO", + "Cockpit briefing carousel": "Carrousel de briefing du cockpit", + "Estimated flight plan": "Plan de vol estimé", + "ESTIMATED FLIGHT PLAN": "PLAN DE VOL ESTIMÉ", + "FROM": "DE", + "TO": "VERS", + "Cockpit briefing controls": "Commandes du briefing du cockpit", + "Previous briefing page": "Page de briefing précédente", + "Next briefing page": "Page de briefing suivante", + "Live signals": "Signaux en direct", + "Latest regional news": "Dernières actualités régionales", + "Location-based information": "Informations selon le lieu", + "TEMP": "TEMP", + "WIND": "VENT", + "SKY": "CIEL", + "PRECIP": "PRÉCIP", + "MM": "MM", + "Weather data by Open-Meteo.com": "Données météo d'Open-Meteo.com", + "Cockpit briefing pages": "Pages du briefing du cockpit", + "SIG": "SIG", + "Show Live Signals": "Afficher les signaux en direct", + "NEWS": "ACTUS", + "Show Regional News": "Afficher les actus régionales", + "LOCAL": "LOCAL", + "Show Local Info": "Afficher les infos locales", + "Cockpit display and Radio controls": "Commandes d'affichage et de Radio du cockpit", + "DISPLAY": "AFFICHAGE", + "Cockpit display options": "Options d'affichage du cockpit", + "RADIO": "RADIO", + "Cockpit compact Radio controls": "Commandes Radio compactes du cockpit", + "Previous filtered radio station": "Station radio filtrée précédente", + "Play selected radio station": "Écouter la station radio sélectionnée", + "Next filtered radio station": "Station radio filtrée suivante", + "VOLUME": "VOLUME", + "Cockpit Radio volume": "Volume de la Radio du cockpit", + "ESC EXIT": "ESC QUITTER", + "C TOGGLE": "C BASCULER", + "View switcher": "Sélecteur de vue", + "Exit cockpit and return to full globe view": "Quitter le cockpit et revenir à la vue du globe entier", + "RESET": "RÉINIT.", + "Exit cockpit view": "Quitter la vue cockpit", + "EXIT COCKPIT": "QUITTER LE COCKPIT", + "Navigation, voice, and visual preset controls": "Commandes de navigation, de voix et de préréglages visuels", + "Expand Visual Presets": "Développer les préréglages visuels", + "Pin visual presets": "Épingler les préréglages visuels", + "Keep visual presets open": "Garder les préréglages visuels ouverts", + "Show the globe without a visual filter.": "Afficher le globe sans filtre visuel.", + "Emulate a green phosphor CRT with scanlines and screen curvature.": "Émuler un écran CRT à phosphore vert avec lignes de balayage et courbure d'écran.", + "Simulate night-vision goggles with green intensification and a tube vignette.": "Simuler des jumelles de vision nocturne avec intensification verte et vignettage du tube.", + "Simulate FLIR-style thermal contrast. Turn up Ironbow for color.": "Simuler un contraste thermique de type FLIR. Montez Ironbow pour la couleur.", + "Apply bright cel-shaded color and illustrated outlines.": "Appliquer des couleurs vives en cel-shading et des contours illustrés.", + "Apply high-contrast monochrome film-noir grading.": "Appliquer un étalonnage film noir monochrome à fort contraste.", + "MAP SOURCE": "SOURCE CARTE", + "Map source": "Source de la carte", + "Style": "Style", + "Expand LOCATION": "Développer LIEU", + "Collapse panel": "Réduire le panneau", + "Pin location tray": "Épingler le tiroir des lieux", + "Keep location tray open": "Garder le tiroir des lieux ouvert", + "Search any location": "Rechercher n'importe quel lieu", + "Search location by name or coordinates": "Rechercher un lieu par nom ou coordonnées", + "Search any location...": "Rechercher un lieu…", + "WEATHER": "MÉTÉO", + "Active weather products": "Produits météo actifs", + "RECENT IMAGERY": "IMAGERIE RÉCENTE", + "Imagery days in the box": "Jours d'imagerie dans la zone", + "CONTEXT": "CONTEXTE", + "Expand panel": "Développer le panneau", + "Context mode": "Mode de contexte", + "CONTACTS": "CONTACTS", + "Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "Parcourt les contacts les plus proches du type sélectionné — avions, navires, installations. Les satellites sont suivis séparément.", + "SPACE MISSIONS": "MISSIONS SPATIALES", + "SELECT CONTEXT": "CHOISIR UN CONTEXTE", + "Contact Context actions": "Actions du contexte Contact", + "COCKPIT": "COCKPIT", + "SEARCH NEARBY SITES": "CHERCHER SITES PROCHES", + "Reclassify tracked contact as TR-3B": "Reclasser le contact suivi en TR-3B", + "CONTACTS CONTEXT OFF": "CONTEXTE CONTACTS DÉSACTIVÉ", + "SELECT CONTACTS TO LOAD OBSERVED / MAPPED PROXIMITY": "CHOISIR CONTACTS POUR VOIR LES CONTACTS PROCHES OBSERVÉS / CARTOGRAPHIÉS", + "Available Space Missions": "Missions spatiales disponibles", + "AVAILABLE MISSIONS": "MISSIONS DISPONIBLES", + "SELECT A MISSION TO INSPECT": "CHOISIR UNE MISSION À EXAMINER", + "LOADING 30-DAY MISSION INDEX": "CHARGEMENT DE L'INDEX DES MISSIONS À 30 JOURS", + "TAB PREVIEWS · ENTER / SPACE SELECTS": "TAB APERÇU · ENTER / SPACE SÉLECTIONNE", + "Internet radio and local SDR": "Radio Internet et SDR local", + "STATION TAG": "TAG DE STATION", + "Filter stations by station tag": "Filtrer les stations par tag de station", + "All": "Tous", + "Enable Radio, then choose a globe marker or use next.": "Activez la Radio, puis choisissez un marqueur sur le globe ou utilisez SUIV.", + "DRAG TO TUNE": "GLISSER POUR RÉGLER", + "Tune available internet radio stations": "Régler sur une station de radio Internet disponible", + "ALL · DRAG THE NEEDLE": "TOUTES · DÉPLACER L'AIGUILLE", + "SNAPS TO AVAILABLE STATIONS": "SE CALE SUR LES STATIONS DISPONIBLES", + "Radio playback": "Lecture radio", + "Previous filtered station": "Station filtrée précédente", + "Play selected station": "Écouter la station sélectionnée", + "Next filtered station": "Station filtrée suivante", + "STOP": "ARRÊT", + "Stop radio playback": "Arrêter la lecture radio", + "Radio volume": "Volume de la radio", + "STATION SITE": "SITE DE LA STATION", + "DIRECTORY: RADIO BROWSER": "ANNUAIRE : RADIO BROWSER", + "Audio connects directly to the broadcaster after you press play. Your IP is visible to that broadcaster.": "Le flux audio se connecte directement au diffuseur dès que vous lancez la lecture. Votre IP est visible par ce diffuseur.", + "Local RTL-SDR receiver": "Récepteur 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 · un seul tuner local partagé entre la FM et la couche ADS-B local.", + "Use your location for faster ADS-B position decoding": "Utiliser votre position pour décoder plus vite les positions ADS-B", + "CHANGE DEVICE": "CHANGER", + "Choose a different RTL-SDR or receiver channel": "Choisir un autre RTL-SDR ou un autre canal du récepteur", + "RTL-SDR receiver mode": "Mode du récepteur RTL-SDR", + "GAIN": "GAIN", + "RTL-SDR tuner gain for the current mode": "Gain du tuner RTL-SDR pour le mode actuel", + "AUTO": "AUTO", + "MSG/S": "MSG/S", + "HEARD": "CAPTÉS", + "POSITIONED": "POSITIONNÉS", + "FM MHZ": "FM MHZ", + "TUNE": "RÉGLER", + "Seek broadcast FM stations": "Rechercher des stations FM", + "◀ SEEK": "◀ RECH.", + "SEEK ▶": "RECH. ▶", + "SDR VOL": "VOL. SDR", + "Local SDR volume": "Volume du SDR local", + "Intelligence HUD (H)": "HUD de renseignement (H)", + "Layout": "Disposition", + "HUD layout": "Disposition du HUD", + "Tactical": "Tactique", + "Minimal": "Minimal", + "Cyber": "Cyber", + "Sonar": "Sonar", + "Cyber sonar contact scanner": "Scanner de contacts du sonar Cyber", + "Rings": "Anneaux", + "Sonar ring count": "Nombre d'anneaux du sonar", + "Range": "Portée", + "Sonar ring range": "Portée des anneaux du sonar", + "Power": "Puissance", + "Sonar intensity": "Intensité du sonar", + "Opacity": "Opacité", + "Cyber contact and label opacity": "Opacité des contacts et étiquettes Cyber", + "Minimum opacity for map dots, icons, models, brackets, and labels between sonar passes": "Opacité minimale des points, icônes, modèles, cadres de ciblage et étiquettes de la carte entre deux balayages du sonar", + "Sector": "Secteur", + "Sonar sector width": "Largeur du secteur du sonar", + "Detection Overlay (D)": "Surcouche de détection (D)", + "Detection overlay": "Surcouche de détection", + "Detection label density": "Densité des étiquettes de détection", + "Allocation": "Allocation", + "Detection label allocation": "Allocation des étiquettes de détection", + "Elastic": "Élastique", + "Weighted": "Pondérée", + "Fade": "Fondu", + "Detection fade distance": "Distance de fondu de la détection", + "World-overlay fade distance outside the keyhole as a percentage of its radius": "Distance de fondu de la surcouche hors de l'ouverture, en pourcentage de son rayon", + "Outside": "Extérieur", + "Detection opacity outside the keyhole": "Opacité de la détection hors de l'ouverture", + "World-overlay label and card opacity beyond the fade distance": "Opacité des étiquettes et fiches de la surcouche au-delà de la distance de fondu", + "PARAMETERS": "PARAMÈTRES", + "3D aircraft — flat icons zoomed out, 3D models up close": "Aéronefs 3D — icônes plates de loin, modèles 3D de près", + "Models": "Modèles", + "3D model coverage": "Couverture des modèles 3D", + "Proximity": "Proximité", + "Scope — the circular viewport mask": "Lunette — le masque circulaire de la vue", + "Scope": "Lunette", + "Feather": "Adouci", + "Scope edge feather": "Adoucissement du bord de la lunette", + "Scope edge feather as a percentage of the keyhole radius": "Adoucissement du bord de la lunette, en pourcentage du rayon de l'ouverture", + "Draw on the world — click vertices, double-click or Enter to finish, Esc to cancel": "Dessinez sur le monde — cliquez sur les sommets, double-clic ou Enter pour terminer, Esc pour annuler", + "Shape": "Forme", + "Shape to draw": "Forme à dessiner", + "Area": "Zone", + "Line": "Ligne", + "Pin": "Épingle", + "Label (optional)": "Étiquette (facultative)", + "Label for the drawn shape": "Étiquette de la forme dessinée", + "Colour of the drawn shape": "Couleur de la forme dessinée", + "Primary": "Principale", + "Amber": "Ambre", + "Cyan": "Cyan", + "Green": "Vert", + "Red": "Rouge", + "Clear": "Effacer", + "Remove every mark from the board": "Retirer toutes les marques du tableau", + "Show your own GeoJSON, KML, KMZ or GPX files on the globe (or drop them onto it). File bytes stay on this device; overlays can appear in map screenshots shared with voice.": "Affichez vos propres fichiers GeoJSON, KML, KMZ ou GPX sur le globe (ou déposez-les dessus). Les fichiers restent sur cet appareil ; les surcouches peuvent apparaître dans les captures de carte partagées avec la voix.", + "Import": "Importer", + "Choose GeoJSON, KML, KMZ or GPX files": "Choisir des fichiers GeoJSON, KML, KMZ ou GPX", + "Imported files": "Fichiers importés", + "Celestial": "Céleste", + "Hide UI chrome": "Masquer l'interface", + "Clean UI": "Vue épurée", + "Bloom / Glow": "Bloom / Lueur", + "Bloom intensity": "Intensité du bloom", + "Sharpening": "Netteté", + "Sharpen": "Netteté", + "Sharpen intensity": "Intensité de la netteté", + "EXIT CLEAN VIEW": "QUITTER LA VUE ÉPURÉE", + "Return UI controls": "Rétablir les commandes de l'interface", + "Initializing photorealistic world...": "Initialisation du monde photoréaliste…", + "DATA LAYERS": "COUCHES", + "ACQUIRING FRAME": "ACQUISITION DE L'IMAGE", + "NEAREST": "LA PLUS PROCHE", + "CCTV camera": "Caméra CCTV", + "CALIBRATION": "CALIBRATION", + "Drag the camera in the world: rings rotate, arrows move, handles set range/FOV": "Faites glisser la caméra dans le monde : les anneaux pivotent, les flèches déplacent, les poignées règlent portée/FOV", + "Camera pose — click a value to type": "Pose de la caméra — cliquer sur une valeur pour la saisir", + "Heading (compass °) — click to type": "Cap (boussole °) — cliquer pour saisir", + "Pitch (° up/down) — click to type": "Inclinaison (° haut/bas) — cliquer pour saisir", + "Horizontal FOV (°) — click to type": "FOV horizontal (°) — cliquer pour saisir", + "Range / monitor-plane distance (m) — click to type": "Portée / distance du plan moniteur (m) — cliquer pour saisir", + "Mount height above ground (m) — click to type": "Hauteur de montage au-dessus du sol (m) — cliquer pour saisir", + "North offset from catalog position (m) — click to type": "Décalage nord par rapport à la position du catalogue (m) — cliquer pour saisir", + "East offset from catalog position (m) — click to type": "Décalage est par rapport à la position du catalogue (m) — cliquer pour saisir", + "SAVE CAL": "ENREG. CAL", + "RESET CAL": "RÉINIT. CAL", + "SCENE SUMMARY": "RÉSUMÉ DE SCÈNE", + "STREET LEVEL": "NIVEAU RUE", + "Turn Street Level on or off": "Activer ou désactiver Niveau rue", + "Show the street-level view full screen (Esc shrinks it)": "Afficher la vue au niveau de la rue en plein écran (Esc la réduit)", + "Image fit": "Ajustement de l'image", + "FIT": "AJUSTER", + "Fit: show the whole photo, with dark bars where its shape differs from the frame": "Ajuster : affiche toute la photo, avec des bandes sombres là où son format diffère du cadre", + "FILL": "REMPLIR", + "Fill: fill the frame edge to edge, cropping the photo": "Remplir : remplit le cadre bord à bord en recadrant la photo", + "Close the image and deselect it on the map": "Fermer l'image et la désélectionner sur la carte", + "Close the street-level image": "Fermer l'image au niveau de la rue", + "Street-level viewer": "Visionneuse au niveau de la rue", + "Open this image on the provider's site": "Ouvrir cette image sur le site du fournisseur", + "Imagery providers": "Fournisseurs d'images", + "PROVIDERS": "FOURNISSEURS", + "Imagery filters": "Filtres d'images", + "IMAGERY": "IMAGES", + "Panorama filter": "Filtre de panoramas", + "ALL": "TOUTES", + "FLAT": "CLASSIQUES", + "SINCE": "DEPUIS", + "SCENES": "SCÈNES", + "Scene recipe": "Recette de scène", + "NEW": "NOUV.", + "CAPTURE SHOT": "CAPTURER UN PLAN", + "UPDATE SHOT": "MAJ DU PLAN", + "START": "LANCER", + "EXPORT PRESETS": "EXPORTER LES PRÉRÉGLAGES", + "IMPORT": "IMPORTER", + "RUN LOG": "JOURNAL", + "POWER UP": "ALLUMER", + "GROUND STATION · PROVIDER SETTINGS": "STATION AU SOL · PARAMÈTRES DES FOURNISSEURS", + "Close key setup": "Fermer la configuration des clés", + "Power up the globe": "Allumer le globe", + "The globe already flies keyless. Every key below switches on another real feed — paste one and it's saved into this app's local configuration, then the server restarts itself. Server-side keys stay on this machine; Google Maps and Cesium ion run in the browser and must be provider-restricted. Keys you configured elsewhere are shown but never touched.": "Le globe fonctionne déjà sans clé. Chaque clé ci-dessous active un flux réel de plus : collez-en une et elle est enregistrée dans la configuration locale de l'application, puis le serveur redémarre. Les clés côté serveur restent sur cette machine ; les clés Google Maps et Cesium ion sont utilisées dans le navigateur et doivent être restreintes chez leur fournisseur. Les clés configurées ailleurs sont affichées mais jamais modifiées.", + "SAVE KEYS": "ENREGISTRER LES CLÉS", + "ESC to close": "ESC pour fermer", + "The Google Maps key buys the photorealistic planet — everything else stacks on top.": "La clé Google Maps offre la planète photoréaliste, tout le reste s'y superpose.", + "NO PLACE LEFT BEHIND": "AUCUN LIEU OUBLIÉ", + "ACTIVE STYLE": "STYLE ACTIF", + "Globe actions": "Actions du globe", + "Turn off all selected data layers": "Désactiver toutes les couches de données sélectionnées", + "Copy share link": "Copier le lien de partage", + "Toggle straight-down and tilted map views": "Basculer entre vue verticale et vue inclinée de la carte", + "Reset map to north up": "Réorienter la carte nord en haut", + "Reset map bearing to north": "Réinitialiser l'orientation de la carte vers le nord", + "Reset camera and return to full globe view": "Réinitialiser la caméra et revenir à la vue du globe entier", + "MISSION CONTROL · FIRST LAUNCH": "CENTRE DE CONTRÔLE · PREMIER LANCEMENT", + "Choose your first view": "Choisissez votre première vue", + "LIVE CONTACTS": "CONTACTS EN DIRECT", + "Aircraft, vessels and nearby intelligence": "Avions, navires et renseignements à proximité", + "Launches, spacecraft and orbital context": "Lancements, vaisseaux et contexte orbital", + "EXPLORE MANUALLY": "EXPLORER MANUELLEMENT", + "Begin with a clean globe": "Commencer avec un globe vierge", + "Don't show this again": "Ne plus afficher", + "ESC to dismiss": "ESC pour fermer", + "Tip: the GEV MIC button in the dock lets you talk to the map.": "Astuce : le bouton GEV MIC du dock permet de parler à la carte.", + "Malformed cyclone snapshot": "Instantané cyclonique mal formé", + "Cyclone request timed out": "Délai dépassé pour la requête cyclonique" +} diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index 4c141120b..e9a4c76ac 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -63,7 +63,7 @@ "Gyirong border gate": "Valico di frontiera di Gyirong", "Border gate": "Valico di frontiera", "Timure evidence cluster": "Gruppo di prove di Timure", - "Mailung / Upper Trishuli bridge": "Ponte di Mailung / Alto Trishuli", + "Mailung / Upper Trishuli bridge": "Ponte di Mailung / Upper Trishuli", "Dandagaun viewpoint restaurant": "Ristorante panoramico di Dandagaun", "Dandagaun viewpoint": "Punto panoramico di Dandagaun", "Bidur - Trishuli bridge": "Ponte di Bidur - Trishuli", @@ -1625,6 +1625,11 @@ "many": "{count} di telecamere caricate · attiva CCTV per attivarle", "other": "{count} telecamere caricate · attiva CCTV per attivarle" }, + "{count} more cameras outside this view · pan or zoom out": { + "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.", "No summary available.": "Nessun riepilogo disponibile.", @@ -1957,10 +1962,6 @@ "STATION UNAVAILABLE": "STAZIONE NON DISPONIBILE", "Station unavailable after directory refresh": "Stazione non disponibile dopo l'aggiornamento dell'elenco", "Radio could not {action} cleanly": "La Radio non ha potuto {action} correttamente", - "Go to expanded Radio section": "Vai alla sezione Radio espansa", - "Expand Radio section in Context": "Espandi la sezione Radio in Contesto", - "Close compact Radio controls": "Chiudi i controlli Radio compatti", - "Open compact Radio controls": "Apri i controlli Radio compatti", "SYNC": "SYNC", "RECONCILE": "RICONCILIA", "DISABLE": "DISATTIVA", @@ -1983,9 +1984,6 @@ "selected": "selezionata", "nearest": "più vicina", "{action} {target} radio station": "{action}: stazione radio {target}", - "RADIO STATE UNCERTAIN": "STATO RADIO INCERTO", - "SYNCING DIRECTORY": "SINCRONIZZAZIONE ELENCO", - "RADIO READY": "RADIO PRONTA", "SYNCING": "SYNC IN CORSO", "READY": "PRONTA", "station": "stazione", @@ -2795,11 +2793,6 @@ "RECENT IMAGERY": "IMMAGINI RECENTI", "Imagery days in the box": "Giorni con immagini nel riquadro", "CONTEXT": "CONTESTO", - "Compact Radio controls": "Controlli Radio compatti", - "Open detailed Radio controls": "Apri i controlli Radio dettagliati", - "Previous station": "Stazione precedente", - "Next station": "Stazione successiva", - "Compact Radio volume": "Volume Radio (controlli compatti)", "Expand panel": "Espandi il pannello", "Context mode": "Modalità contesto", "CONTACTS": "CONTATTI", @@ -2860,7 +2853,6 @@ "Layout": "Layout", "HUD layout": "Layout HUD", "Tactical": "Tattico", - "Operator": "Operatore", "Minimal": "Minimale", "Cyber": "Cyber", "Sonar": "Sonar", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json new file mode 100644 index 000000000..5a48dc886 --- /dev/null +++ b/src/i18n/locales/ja.json @@ -0,0 +1,2860 @@ +{ + "Pick a shape, then click the map.": "図形を選んでから、地図をクリックしてください。", + "Enter to place the pin, Esc to cancel.": "Enterでピンを配置、Escでキャンセル。", + "Click where the pin goes.": "ピンを置く場所をクリックしてください。", + "Click {count} more points.": { + "other": "あと{count}点クリックしてください。" + }, + "Those points are in a line — move one off it to enclose an area.": "点が一直線上に並んでいます。範囲を囲むには、いずれかの点を線から外してください。", + "That line has no length — click somewhere further away.": "線の長さがゼロです。もっと離れた場所をクリックしてください。", + " · {max}-point limit reached": " · 上限の{max}点に達しました", + "{measure} · double-click or Enter to finish, Backspace undoes, Esc cancels.{full}": "{measure} · ダブルクリックまたはEnterで完了、Backspaceで元に戻す、Escでキャンセル。{full}", + "drawings on the map": "地図上の描画", + "That shape already has {count} points — finish it or press Backspace.": "この図形はすでに{count}点あります。完了するか、Backspaceを押してください。", + "That point is off the globe — click on the world.": "その点は地球の外です。地表をクリックしてください。", + "That shape could not be placed.": "図形を配置できませんでした。", + "Could not place the shape: {error}": "図形を配置できませんでした:{error}", + "Board cleared.": "ボードを消去しました。", + "{owner} is using the pointer — close it first.": "{owner}がポインターを使用中です。先に閉じてください。", + "{label} · Detailed outline unavailable": "{label} · 詳細な輪郭を取得できません", + "Flying to Austin, TX...": "テキサス州オースティンへ移動中…", + "Restoring shared view...": "共有ビューを復元中…", + "Configuring viewer...": "ビューアーを設定中…", + "Loading Google 3D Tiles...": "Google 3D Tilesを読み込み中…", + "Loading the keyless globe...": "キー不要の地球儀を読み込み中…", + "Google 3D Tiles unavailable ({detail}). Loading the keyless globe...": "Google 3D Tilesを利用できません({detail})。キー不要の地球儀を読み込み中…", + "Initializing systems...": "システムを初期化中…", + "Space Missions isolates replay data. Exit the mode to enable {layer}.": "宇宙ミッションはリプレイデータを分離しています。{layer}を有効にするには、このモードを終了してください。", + "Data attribution": "データの出典", + "Data provided by:": "データ提供:", + "Close data attribution": "データ出典を閉じる", + "Light aircraft": "軽飛行機", + "Glider": "グライダー", + "Turboprop": "ターボプロップ機", + "Airliner": "旅客機", + "Widebody": "ワイドボディ機", + "Four-engine jet": "4発ジェット機", + "Helicopter": "ヘリコプター", + "Fast jet": "高速ジェット機", + "Business jet": "ビジネスジェット", + "Drone": "ドローン", + "General emergency": "緊急事態", + "Unlawful interference": "不法妨害", + "Radio failure": "通信機故障", + "Downed aircraft": "墜落機", + "Minimum fuel": "ミニマムフューエル", + "Lifeguard (medical priority)": "救急飛行(医療優先)", + "Rasuwagadhi witness video": "Rasuwagadhiの目撃映像", + "Bhote Koshi valley footage": "Bhote Koshi渓谷の映像", + "Flood footage": "洪水の映像", + "Compilation and casualty claims": "映像まとめと死傷者に関する主張", + "SOURCE GEOCONFIRMED · WORLD-PINNED": "ソース位置確認済み · 地図に固定", + "SOURCE CREDITED · LOCATION UNRESOLVED": "ソース明記 · 位置未特定", + "REFERENCE ONLY · CLAIMS UNVERIFIED": "参考情報のみ · 主張は未検証", + "SOURCE GEOCONFIRMED": "ソース位置確認済み", + "2021 historical reference versus 2026 post-event imagery; changes are cumulative and include the July 2025 Rasuwagadhi flood. Schematic downstream progression, not modeled arrival time; not an official hazard model.": "2021年の過去参照画像と2026年の発生後画像の比較です。変化は累積的で、2025年7月のRasuwagadhi洪水を含みます。下流への進行は模式図であり、到達時刻のモデル計算ではありません。公式のハザードモデルでもありません。", + "Immediate collapse viewpoint": "崩壊地点直近の視点", + "Collapse viewpoint": "崩壊地点の視点", + "Debris-dammed lake": "土砂ダム湖", + "Dammed lake": "土砂ダム湖", + "Second landslide": "第2の地すべり", + "Gyirong border gate": "Gyirong国境ゲート", + "Border gate": "国境ゲート", + "Timure evidence cluster": "Timureの証拠群", + "Mailung / Upper Trishuli bridge": "Mailung / Upper Trishuli橋", + "Dandagaun viewpoint restaurant": "Dandagaun展望レストラン", + "Dandagaun viewpoint": "Dandagaun展望地点", + "Bidur - Trishuli bridge": "Bidur - Trishuli橋", + "Bidur bridge": "Bidur橋", + "Devighat / Taadi Khola Bridge": "Devighat / Taadi Khola橋", + "Devighat bridge": "Devighat橋", + "Facebook player API did not initialize.": "FacebookプレーヤーAPIが初期化されませんでした。", + "Facebook player API could not load.": "FacebookプレーヤーAPIを読み込めませんでした。", + "Facebook player API timed out.": "FacebookプレーヤーAPIがタイムアウトしました。", + "X embed API did not initialize.": "X埋め込みAPIが初期化されませんでした。", + "X embed could not load.": "X埋め込みを読み込めませんでした。", + "X embed timed out.": "X埋め込みがタイムアウトしました。", + "UNAVAILABLE": "利用不可", + "TIMEOUT": "タイムアウト", + "BLOCKED": "ブロック", + "SOURCE": "ソース", + "Facebook embed unavailable. The post may be private, removed, region-restricted, or blocked.": "Facebookの埋め込みを利用できません。投稿が非公開、削除済み、地域制限中、またはブロックされている可能性があります。", + "Preloading {provider} media": "{provider}のメディアを事前読み込み中", + "LOADING X POST…": "Xの投稿を読み込み中…", + "The X post is unavailable or cannot be embedded.": "このXの投稿は利用できないか、埋め込みできません。", + "X POST · PROVIDER CONTROLS": "Xの投稿 · 提供元で操作", + "{reason} OPEN ORIGINAL.": "{reason}元の投稿を開いてください。", + "X embed unavailable": "Xの埋め込みを利用できません。", + "FACEBOOK · PROVIDER CONTROLS": "Facebook · 提供元で操作", + "Facebook embed unavailable": "Facebookの埋め込みを利用できません。", + "AUTOPLAY BLOCKED · USE PROVIDER CONTROLS": "自動再生ブロック · 提供元で操作", + "Embedded {provider} media for {source}": "{source}の埋め込み{provider}メディア", + "witness source": "目撃ソース", + "Witness source": "目撃ソース", + "{provider} · EMBEDDED SOURCE": "{provider} · 埋め込みソース", + "OPEN ORIGINAL ↗": "元の投稿を開く ↗", + "{provider} media: {source}": "{provider}メディア:{source}", + "{provider} · PROVIDER CONTROLS": "{provider} · 提供元で操作", + "YOUTUBE {state} · OPEN ORIGINAL": "YouTube {state} · 元の投稿を開く", + "INITIATION": "発生", + "DAMMING": "せき止め", + "SECONDARY FAILURE": "二次崩壊", + "BORDER ARRIVAL": "国境到達", + "PROPAGATION": "伝播", + "DOWNSTREAM PASSAGE": "下流通過", + "CONSEQUENCE": "影響", + "DOWNSTREAM CONSEQUENCE": "下流への影響", + "EVIDENCE": "証拠", + "{year} HISTORICAL REFERENCE": "{year}年 過去参照", + "{year} POST-EVENT": "{year}年 発生後", + "{count} GEOLOCATED SOURCE-MAP PLACEMENTS": { + "other": "ソースマップ上の位置特定 {count}件" + }, + "NO PREVIEW": "プレビューなし", + "YouTube source": "YouTubeソース", + "Facebook source": "Facebookソース", + "SOURCE LINK": "ソースリンク", + "{publisher} / OPEN": "{publisher} / 開く", + "Bhote Koshi flood reconstruction controls": "Bhote Koshi洪水再現の操作", + "EVENT RECONSTRUCTION · 26 AUG 2026": "イベント再現 · 2026年8月26日", + "BHOTE KOSHI OUTBURST FLOOD": "BHOTE KOSHI決壊洪水", + "Close event layer": "イベントレイヤーを閉じる", + "OBSERVED IMAGERY": "観測画像", + "SCHEMATIC CORRIDOR": "模式的な流路", + "2021 HISTORICAL REFERENCE / 2026 POST-EVENT": "2021年 過去参照 / 2026年 発生後", + "Historical reference and post-event image split": "過去参照画像と発生後画像の分割表示", + "RECONSTRUCTION CLOCK": "再現クロック", + "Schematic downstream progression": "下流への進行(模式)", + "Story beat navigation": "ストーリー区切りのナビゲーション", + "Previous story beat": "前のストーリー区切り", + "CAUSE": "原因", + "CAPTURE TIME UNVERIFIED": "撮影時刻未検証", + "Next story beat": "次のストーリー区切り", + "▶ PLAY": "▶ 再生", + "Play the next shot and continue through this scene": "次のショットを再生し、このシーンを最後まで続けます", + "▶ PLAY SCENE": "▶ シーン再生", + "◉ CINEMATIC": "◉ シネマティック", + "↺ FULL STORY": "↺ 全ストーリー", + "↗ OPEN ORIGINAL": "↗ 元の投稿を開く", + "⌖ LOWER GORGE": "⌖ 下流峡谷", + "FIELD REPORTS · {count}": "現地報告 · {count}", + "GEOLOCATIONS · {credit}": "位置特定 · {credit}", + "↗ OPEN PUBLIC MAP": "↗ 公開マップを開く", + " Clouds are preserved from the source imagery.": "雲は元画像のまま残しています。", + "Rasuwagadhi witness": "Rasuwagadhiの目撃者", + "Open original source for {source}; capture time unverified": "{source}の元ソースを開く(撮影時刻未検証)", + "Historical reference and post-event image divider": "過去参照画像と発生後画像の境界線", + "A historical reference {before} percent, B post-event {after} percent": "A(過去参照){before}%、B(発生後){after}%", + "■ RELEASE CAMERA": "■ カメラを解放", + "Seek Nepal scene clock": "ネパールのシーンクロックをシーク", + "NEPAL SCENE CLOCK": "ネパール シーンクロック", + "… PLAYING SHOT": "… ショット再生中", + "▶ PLAY SHOT": "▶ ショット再生", + "Ⅱ PAUSE": "Ⅱ 一時停止", + "↺ REPLAY CINEMATIC": "↺ シネマティックを再生", + "↺ REPLAY": "↺ リプレイ", + "… PLAYING SCENE": "… シーン再生中", + "Continue {scene} from next shot": "次のショットから{scene}を続行", + "current scene": "現在のシーン", + "Replay the full reconstruction": "再現全体をリプレイ", + "SCENE SHOT": "シーンショット", + "TIME UNVERIFIED": "撮影時刻未検証", + "{count} SOURCES": { + "other": "ソース {count}件" + }, + "OUTSIDE IMAGERY SWIPE": "画像比較の範囲外", + "{scene} / AUTHORED SHOT": "{scene} / 作成済みショット", + "NEPAL FLOOD INCIDENT": "ネパール洪水インシデント", + "Play {shot}": "{shot}を再生", + "current scene shot": "現在のシーンショット", + "Play reconstruction": "再現を再生", + "Load previous scene shot": "前のシーンショットを読み込む", + "Load next scene shot": "次のシーンショットを読み込む", + "SCHEMATIC CORRIDOR, NOT MODELED ARRIVAL TIME.": "模式的な流路であり、到達時刻のモデル計算ではありません。", + "Rasuwa · 26 Aug 2026": "Rasuwa · 2026年8月26日", + "Event reconstruction": "イベント再現", + "BHOTE KOSHI FLOOD INCIDENT": "BHOTE KOSHI洪水インシデント", + "NEPAL · 26 AUG 2026": "ネパール · 2026年8月26日", + "USGS INITIAL REPORT": "USGS初期報告", + "26 AUG 2026 · 08:37 NPT": "2026年8月26日 · 08:37 NPT", + "28.2710° N · 85.5150° E": "北緯28.2710° · 東経85.5150°", + "INITIAL M4.4 · RECLASSIFIED M5.2 LANDSLIDE": "初期 M4.4 · M5.2の地すべりに再分類", + "USGS EVENT us7000tbwb · OPEN SOURCE": "USGSイベント us7000tbwb · ソースを開く", + "Open the USGS initial report for the 26 August 2026 Nepal landslide": "2026年8月26日のネパール地すべりに関するUSGS初期報告を開く", + "Show the Nepal context callout without a border or country fill": "国境線や国の塗りつぶしなしで、ネパールの説明ラベルを表示", + "Introduce Nepal, draw its border, then reveal the Bhote Koshi incident": "ネパールを紹介して国境を描き、Bhote Koshiのインシデントを表示", + "CITIES": "都市", + "Keep the regional incident context and introduce nearby city labels": "地域のインシデント情報を残したまま、周辺都市のラベルを表示", + "PLACES": "地点", + "Introduce the reconstruction scene key places from trigger to downstream": "再現シーンの主要地点を、発生地点から下流まで順に紹介", + "PATH": "経路", + "Animate the sourced cyan river-centerline route to Trishuli Bazaar": "出典に基づくシアンの河道中心線をTrishuli Bazaarまでアニメーション表示", + "OVERVIEW": "概観", + "Keep the completed flood path and location labels visible": "完成した洪水経路と地点ラベルを表示したままにする", + "RECORD": "記録", + "Reveal the trigger record while making a slow camera approach": "カメラをゆっくり近づけながら、発生記録を表示", + "{speed} kn": "{speed} kn", + "That file is not valid JSON.": "このファイルは有効なJSONではありません。", + "That JSON file is not GeoJSON.": "このJSONファイルはGeoJSONではありません。", + "That FeatureCollection has no features list.": "このFeatureCollectionにはfeaturesリストがありません。", + "Every GeoJSON feature must have type Feature.": "GeoJSONのすべてのフィーチャーはtypeがFeatureである必要があります。", + "That GeoJSON has invalid geometries.": "このGeoJSONには無効なジオメトリがあります。", + "That GeoJSON contains an unsupported geometry.": "このGeoJSONには対応していないジオメトリが含まれています。", + "That GeoJSON has invalid coordinates.": "このGeoJSONには無効な座標があります。", + "That file creates more than {limit} map objects (the limit).": "このファイルから作成される地図オブジェクトが上限の{limit}個を超えています。", + "That file has more than {limit} style definitions and image links after cleanup (the limit).": "このファイルは、クリーンアップ後もスタイル定義と画像リンクが上限の{limit}個を超えています。", + "That file has more than {limit} features (the limit).": "このファイルのフィーチャーが上限の{limit}個を超えています。", + "That file has more than {limit} points (the limit) across {features} features.": "このファイルの点が上限の{limit}個を超えています(フィーチャー数:{features})。", + "That file has more than {limit} points (the limit).": "このファイルの点が上限の{limit}個を超えています。", + "That KMZ unpacks to more than the import limit.": "このKMZは展開するとインポート上限を超えます。", + "That KMZ is damaged and could not be unpacked.": "このKMZは破損しているため、展開できませんでした。", + "That KMZ is not a readable zip archive.": "このKMZは読み取り可能なzipアーカイブではありません。", + "That KMZ uses a zip format this import does not read.": "このKMZは、このインポートで読み取れないzip形式を使用しています。", + "That KMZ is encrypted.": "このKMZは暗号化されています。", + "That KMZ uses a compression method this import does not read.": "このKMZは、このインポートで読み取れない圧縮方式を使用しています。", + "That KML has invalid geometry bounds.": "このKMLのジオメトリ範囲が無効です。", + "That KML has invalid coordinates.": "このKMLには無効な座標があります。", + "That GPX file has no document.": "このGPXファイルにはドキュメントがありません。", + "That {format} file is not valid XML.": "この{format}ファイルは有効なXMLではありません。", + "That file has an unreadable image or exceeds the 16-megapixel image limit.": "このファイルには読み取れない画像があるか、画像が1,600万画素の上限を超えています。", + "That KML could not be read.": "このKMLを読み取れませんでした。", + "That file has invalid point coordinates.": "このファイルには無効な点座標があります。", + "Ground heights are not available for that file. Try again when the map has loaded.": "このファイルの地表高を取得できません。地図の読み込み後にもう一度お試しください。", + "That file took too long to read.": "ファイルの読み取りに時間がかかりすぎました。", + "Nothing to import.": "インポートするものがありません。", + "That file is larger than {size} MiB.": "このファイルは{size} MiBを超えています。", + "Only GeoJSON, KML, KMZ and GPX files can be imported.": "インポートできるのはGeoJSON、KML、KMZ、GPXファイルのみです。", + "That file has nothing to draw.": "このファイルには描画できる内容がありません。", + "That KMZ holds no KML document.": "このKMZにはKMLドキュメントが含まれていません。", + "That {format} file could not be read.": "この{format}ファイルを読み取れませんでした。", + "That file has {count} map objects; the limit is {limit}.": "このファイルの地図オブジェクトは{count}個です。上限は{limit}個です。", + "Ground heights could not be read. Try that file again.": "地表高を読み取れませんでした。もう一度このファイルをお試しください。", + "That file has invalid point graphics.": "このファイルには無効なポイントグラフィックがあります。", + "Overpass rate-limited": "Overpassがレート制限中", + "Overpass timed out": "Overpassがタイムアウトしました", + "Overpass could not complete the query": "Overpassがクエリを完了できませんでした", + "Map tiles temporarily unavailable": "地図タイルが一時的に利用できません", + "Mapped names temporarily unavailable": "施設名を一時的に取得できません", + "Overpass temporarily unavailable": "Overpassが一時的に利用できません", + "Retrying mapped sites…": "マップ済み施設を再試行中…", + "Fetching mapped sites…": "マップ済み施設を取得中…", + "{reason} — retrying in {seconds}s": "{reason}:{seconds}秒後に再試行", + "{reason} — retry pending": "{reason}:再試行待ち", + "Zoom in to search mapped installations": "ズームインして、マップ済み施設を検索してください", + "Showing cached mapped sites": "キャッシュのマップ済み施設を表示中", + "Mapped sites not loaded": "マップ済み施設は未読込です", + "Mapped sites loaded": "マップ済み施設を読み込みました", + "Military airfield": "軍用飛行場", + "Naval base": "海軍基地", + "Firing range": "射撃場", + "Military land": "軍用地", + "Mapped installation": "マップ済み施設", + "Military area": "軍事区域", + "flight": "フライト", + "military flight": "軍用機", + "satellite": "衛星", + "feed unavailable": "フィードを利用できません", + "feed stale": "フィード更新遅延", + "observed or mapped nearby context": "観測またはマップ済みの周辺コンテキスト", + "no observed or mapped objects in current feeds": "現在のフィードに観測またはマップ済みの対象なし", + "Draw": "描画", + "Directions": "ルート案内", + "Recent imagery": "最近の衛星画像", + "STATION": "宇宙ステーション", + "Crewed stations and their visiting vehicles": "有人宇宙ステーションと、そこを訪れる宇宙機", + "NAV": "測位", + "GNSS navigation — GPS, GLONASS, Galileo": "GNSS測位(GPS、GLONASS、Galileo)", + "GEO": "GEO", + "Geostationary belt — comms and weather, fixed over the equator": "静止軌道帯:通信と気象、赤道上空に固定", + "VISUAL": "可視", + "Brightest naked-eye objects — CelesTrak visual group": "肉眼で見える特に明るい衛星(CelesTrak visualグループ)", + "COMMS": "通信", + "Broadband constellation shell — shown only in DENSE mode": "ブロードバンドコンステレーションのシェル:高密度モードでのみ表示", + "FISHING": "漁船", + "TOWING": "曳航", + "DREDGER": "浚渫船", + "DIVE OPS": "潜水作業", + "MILITARY": "軍用", + "SAILING": "帆船", + "PLEASURE": "プレジャーボート", + "PILOT": "水先船", + "SAR": "捜索救助", + "TUG": "タグボート", + "PORT TENDER": "港湾作業船", + "ANTI-POLLUTION": "汚染防除船", + "LAW ENFORCE": "法執行船", + "MEDICAL": "医療", + "HIGH-SPEED": "高速船", + "PASSENGER": "旅客船", + "CARGO": "貨物船", + "TANKER": "タンカー", + "OTHER": "その他", + "Included in bundle": "バンドルに含まれています", + "Missing bundle file — reimport its bundle": "バンドルファイルがありません。バンドルを再インポートしてください", + "Source configured; file checked when loaded": "ソース設定済み。ファイルは読み込み時に確認します", + "Source unavailable": "ソースを利用できません", + "ENVIRONMENTAL": "環境", + "EARTH WATCH": "地球監視", + "ACTIVE EVENTS": "発生中のイベント", + "Starting live contacts…": "ライブコンタクトを開始中…", + "Opening space missions…": "宇宙ミッションを開いています…", + "Scanning active events…": "発生中のイベントをスキャン中…", + "Live earthquakes and active fires, from USGS and NASA": "USGSとNASAによる地震と火災のライブ情報", + "It feels like a forbidden cockpit—then you realize the sources are public and the data is real.": "禁断のコックピットのようですが、情報源はすべて公開されており、データは本物です。", + "Working…": "処理中…", + "Could not open that mission{detail}. Retry or explore manually.": "ミッションを開けませんでした{detail}。再試行するか、手動で探索してください。", + "This browser is blocking storage, so that could not be saved.": "このブラウザーはストレージをブロックしているため、保存できませんでした。", + "NORMAL": "標準", + "CRT": "CRT", + "NVG": "NVG", + "FLIR": "FLIR", + "ANIME": "アニメ", + "NOIR": "ノワール", + "SNOW": "雪", + "ARCTIC": "北極圏", + "ANTARCTIC": "南極圏", + "NORTH AMERICA": "北米", + "SOUTH AMERICA": "南米", + "EUROPE": "ヨーロッパ", + "AFRICA": "アフリカ", + "ASIA": "アジア", + "OCEANIA": "オセアニア", + "NORTHERN OCEANIC GRID": "北部海洋グリッド", + "SOUTHERN OCEANIC GRID": "南部海洋グリッド", + "LOADING": "読込中", + "DEGRADED": "性能低下", + "STALE": "更新遅延", + "PARTIAL": "部分的", + "FALLBACK": "フォールバック", + "{state} {layers}": "{state} {layers}", + "view band|STREET": "街路", + "view band|CITY": "市街", + "view band|METRO": "都市圏", + "view band|REGIONAL": "地域", + "view band|GLOBAL": "全球", + "SUMMARY": "概要", + "Awaiting telemetry...": "テレメトリー待ち…", + "REC": "REC", + "AIS: --": "AIS: --", + "N/A": "不明", + "{mode} {band} {locality} | {region} | ALT {alt} | WINDOW {window} | SUN {sun}° | ONA {ona}° | {zone}": "{mode} {band} {locality} | {region} | ALT {alt} | 範囲 {window} | SUN {sun}° | ONA {ona}° | {zone}", + "NEAR {poi} ({city}) {distance}": "{poi}({city})付近 {distance}", + "SECTOR {lat} {lon}": "セクター {lat} {lon}", + "hemisphere|N": "N", + "hemisphere|S": "S", + "hemisphere|E": "E", + "hemisphere|W": "W", + "POWER UP · {count} KEYS WAITING": { + "other": "パワーアップ · キー待機 {count}件" + }, + "POWERED UP": "パワーアップ済み", + "Provider Settings does not answer proxied requests": "プロバイダー設定はプロキシ経由のリクエストに応答しません", + "Provider Settings is disabled while sharing is enabled": "共有が有効な間、プロバイダー設定は無効です", + "Provider Settings answers only the machine running the server": "プロバイダー設定は、サーバーを実行しているマシンにのみ応答します", + "Provider Settings answers only local hostnames": "プロバイダー設定はローカルのホスト名にのみ応答します", + "Provider Settings requires an exact local Origin": "プロバイダー設定には完全に一致するローカルのOriginが必要です", + "Unrecognized Origin refused": "認識できないOriginを拒否しました", + "Cross-origin requests are refused": "クロスオリジンのリクエストは拒否されます", + "Content-Type must be application/json": "Content-Typeはapplication/jsonである必要があります", + "Method not allowed": "許可されていないメソッドです", + "Request too large": "リクエストが大きすぎます", + "Invalid JSON": "無効なJSONです", + "Body must be a JSON object of {ENV_VAR: value}": "本文は{ENV_VAR: value}形式のJSONオブジェクトである必要があります", + "No keys provided": "キーが指定されていません", + "At most {limit} keys per save": "1回に保存できるキーは最大{limit}個です", + "Unknown key: {name}": "不明なキー:{name}", + "{name} must be a string": "{name}は文字列である必要があります", + "{name} is empty": "{name}が空です", + "{name} is longer than any real key ({limit} max)": "{name}は実在するどのキーよりも長くなっています(最大{limit}文字)", + "{name} may only contain printable characters with no spaces": "{name}には空白を含まない印字可能文字のみ使用できます", + "{name} contains a character that is not valid in a key (#, quotes, $, \\, or backtick)": "{name}にキーで使用できない文字(#、引用符、$、\\、バッククォート)が含まれています", + "{name} is configured outside Provider Settings and can only be changed where it was set": "{name}はプロバイダー設定の外で設定されているため、設定した場所でのみ変更できます", + "The key was not saved: could not restrict the credential file to your account; nothing was saved": "キーは保存されませんでした:認証情報ファイルへのアクセスをお使いのアカウントに制限できなかったため、何も保存されていません", + "The key was not saved: the existing configuration could not be read, so nothing was changed": "キーは保存されませんでした:既存の設定を読み取れなかったため、何も変更されていません", + "Could not write the {store} store": "{store}ストアに書き込めませんでした", + "Save failed ({status}).": "保存に失敗しました({status})。", + "OAuth check failed: {detail}": "OAuthの確認に失敗しました:{detail}", + "Could not check ChatGPT sign-in. Try again.": "ChatGPTのサインイン状態を確認できませんでした。もう一度お試しください。", + "VOICE AUTH · CHATGPT OAUTH": "音声機能の認証 · ChatGPT OAuth", + "VOICE AUTH · API KEY": "音声機能の認証 · APIキー", + "USE API KEY": "APIキーを使用", + "USE CHATGPT OAUTH": "ChatGPT OAuthを使用", + "Use OPENAI_API_KEY for the next cloud voice session": "次回のクラウド音声セッションでOPENAI_API_KEYを使用", + "Use the signed-in local ChatGPT/Codex OAuth session for the next cloud voice session": "次回のクラウド音声セッションで、このマシンでサインイン済みのChatGPT/Codex OAuthセッションを使用", + "Metered — a billing-enabled account": "従量課金:請求が有効なアカウントが必要", + "Optional paid token — public access works without one": "任意の有料トークン:なくても公開アクセスで利用可能", + "Free key — register, paste, done": "無料キー:登録して貼り付けるだけ", + "browser-side": "ブラウザー側", + "This key runs in the browser by design — restrict it at the provider (see SECURITY.md)": "このキーは仕様上ブラウザーで動作します。プロバイダー側で利用制限を設定してください(SECURITY.mdを参照)", + "configured externally": "外部で設定済み", + "Supplied by your environment, Keychain, or launcher — change it where it was set": "環境変数、キーチェーン、またはランチャーから提供されています。設定した場所で変更してください", + "MANAGE ↗": "管理 ↗", + "GET KEY ↗": "キーを取得 ↗", + "{envVar} saved — paste to replace": "{envVar}は保存済み:貼り付けると置き換えます", + "paste {envVar}": "{envVar}を貼り付け", + "REMOVE": "削除", + "Remove {title} from this app's saved keys": "{title}をこのアプリの保存済みキーから削除", + "unsaved provider keys": "未保存のプロバイダーキー", + "Project keys: {status}": "プロジェクトのキー:{status}", + "your app configuration": "アプリの設定", + "your local .env": "ローカルの .env", + "Saving…": "保存中…", + "Save failed: {detail}": "保存に失敗しました:{detail}", + "Paste at least one key first.": "まず、キーを1つ以上貼り付けてください。", + "Saved to {store}. Restarting — this page reloads itself.": "{store}に保存しました。再起動中です。このページは自動で再読み込みされます。", + "Cloud voice will use OPENAI_API_KEY on the next session.": "次回のセッションから、クラウド音声はOPENAI_API_KEYを使用します。", + "Checking local ChatGPT OAuth sign-in…": "このマシンのChatGPT OAuthサインインを確認中…", + "Could not check ChatGPT sign-in.": "ChatGPTのサインイン状態を確認できませんでした。", + "ChatGPT OAuth selected for cloud voice. Your API key stays saved and available.": "クラウド音声にChatGPT OAuthを選択しました。APIキーは保存されたまま使用できます。", + "ChatGPT sign-in expired. Opening sign-in again…": "ChatGPTのサインインが期限切れです。サインインを再度開いています…", + "Opening ChatGPT sign-in in your browser…": "ブラウザーでChatGPTのサインインを開いています…", + "Could not start ChatGPT sign-in on this machine.": "このマシンでChatGPTのサインインを開始できませんでした。", + "Finish ChatGPT sign-in in the browser. Waiting for it to complete…": "ブラウザーでChatGPTのサインインを完了してください。完了を待っています…", + "ChatGPT sign-in timed out. Click {button} to try again.": "ChatGPTのサインインがタイムアウトしました。[{button}]をクリックしてもう一度お試しください。", + "ChatGPT sign-in complete. OAuth will be used for the next cloud voice session.": "ChatGPTのサインインが完了しました。次回のクラウド音声セッションではOAuthを使用します。", + "Remove this key from your saved configuration?": "このキーを保存済みの設定から削除しますか?", + "Removed from {store}. Restarting — this page reloads itself.": "{store}から削除しました。再起動中です。このページは自動で再読み込みされます。", + "The photorealistic 3D planet, place search + Street View": "フォトリアルな3D地球、場所検索 + ストリートビュー", + "Places context + Street View fallback; optional separate key": "Placesのコンテキスト + ストリートビューの代替。別のキーも指定可", + "Voice control — API key or ChatGPT OAuth": "音声コントロール:APIキーまたはChatGPT OAuth", + "Gemini Live voice control": "Gemini Live音声コントロール", + "Live ships, worldwide": "世界中の船舶をライブ表示", + "Live active-fire detections": "火災のライブ検知", + "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を貼り付けてください", + "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": "測定値がありません", + "No current readings": "現在の測定値がありません", + "{count} stations reporting": { + "other": "{count}局が報告中" + }, + "Highest air-quality readings in view": "表示範囲内で大気質の値が最も高い局", + "Low": "低", + "Low health risk": "健康リスク:低", + "Moderate": "中", + "Moderate health risk": "健康リスク:中", + "High": "高", + "High health risk": "健康リスク:高", + "Very High": "非常に高い", + "Very High health risk": "健康リスク:非常に高い", + "Air quality": "大気質", + "Canada only · ECCC AQHI network": "カナダのみ · ECCC AQHI観測網", + "ECCC request timed out": "ECCCのリクエストがタイムアウトしました", + "Malformed AQHI station response": "AQHI局の応答形式が不正です", + "No AQHI stations available": "利用できるAQHI局がありません", + "Malformed AQHI observation response": "AQHI観測の応答形式が不正です", + "Camera source temporarily unavailable": "カメラのソースが一時的に利用できません", + "Mapped camera locations": "マップ済みのカメラ位置", + "SHOW NEAREST": "最寄りを表示", + "Stop following the current object before navigating to a camera": "カメラへ移動する前に、現在の対象の追従を停止してください", + "Move to the nearest loaded camera and show its details": "読み込み済みの最寄りのカメラへ移動して詳細を表示", + "Camera badges": "カメラバッジ", + "Cyan cameras turn coral when selected. Wedges illustrate mapped direction, not measured coverage. Nearby cameras may be outside the screen.": "シアンのカメラは選択するとコーラルに変わります。扇形はマップされた向きを示すもので、実測のカバー範囲ではありません。周辺のカメラが画面外にある場合があります。", + "{count} nearby · {onScreen} on screen": { + "other": "周辺{count}台 · 画面内{onScreen}台" + }, + "{count} nearby": { + "other": "周辺{count}台" + }, + "retrying mapped ALPR cameras": "マップ済みのALPRカメラを再試行中", + "loading mapped ALPR cameras": "マップ済みのALPRカメラを読み込み中", + "No ALPR data for this area — US and Canada only": "この地域のALPRデータはありません(米国とカナダのみ)", + "Zoom in to load mapped cameras": "ズームインしてマップ済みのカメラを読み込み", + "Showing cached locations": "キャッシュの位置を表示中", + "Coverage limited — zoom in": "カバー範囲が限られています。ズームインしてください", + "No ALPR data for this area": "この地域のALPRデータはありません", + "None on screen — nearby cameras are outside the view": "画面内になし:周辺のカメラは表示範囲外です", + "ALPR camera": "ALPRカメラ", + "Camera coverage unavailable": "カメラのカバー範囲を取得できません", + "OpenStreetMap · community mapped": "OpenStreetMap · コミュニティ作成", + "Overpass returned an incomplete camera response": "Overpassから不完全なカメラデータが返されました", + "At least {count}": "{count}以上", + "CONTEXT READY": "コンテキスト準備完了", + "GLOBAL CONTEXT OFF": "グローバルコンテキスト オフ", + "SELECT A FLIGHT, VESSEL, OR MAPPED INSTALLATION": "フライト、船舶、またはマップ済み施設を選択", + "ENABLE TO LOAD OBSERVED / MAPPED PROXIMITY": "有効にして、観測 / マップ済みの近接情報を読み込み", + "Unavailable": "利用不可", + "Focus {label}": "{label}にフォーカス", + "Global Context navigation": "グローバルコンテキストナビゲーション", + "Previous — prior visited contact in the 250 km window": "前へ:250 km圏内で直前に訪れたコンタクト", + "PREVIOUS": "前へ", + "FOCUS": "フォーカス", + "Next — nearest unvisited contact in the 250 km window": "次へ:250 km圏内で最も近い未訪問のコンタクト", + "NEXT": "次へ", + "Named areas ({n})": "名前付きエリア({n})", + "{distance} FLIGHT / VESSEL WINDOW": "フライト / 船舶の範囲 {distance}", + "Open-source mapped/observed context. Missing broadcasts, unloaded map areas, or unmapped sites are not evidence of absence.": "公開情報に基づくマップ済み/観測済みのコンテキストです。位置送信の欠落、未読込の地図範囲、地図に未登録の施設は、対象が存在しない証拠にはなりません。", + "mapped matches within {km} km of the subject": "対象の{km} km圏内の該当施設", + "none mapped within {km} km; not a complete 250 km survey": "{km} km以内にマップ済み施設なし(250 kmの完全な調査ではありません)", + "mapped matches from the loaded viewport": "読み込み済みの表示範囲内の該当施設", + "viewport feed is not a complete 250 km survey": "表示範囲のフィードは250 kmの完全な調査ではありません", + "HDG {value}°": "HDG {value}°", + "BRG {value}°": "BRG {value}°", + "BRG —": "BRG —", + "CRS {value}°": "CRS {value}°", + "CURRENT VIEWPORT ONLY": "現在の表示範囲のみ", + "WITHIN {km} KM": "{km} km以内", + "Flights": "フライト", + "Military flights": "軍用機", + "AIS vessels": "AIS船舶", + "Mapped installations": "マップ済み施設", + "syncing {count} city feeds...": "{count}都市のフィードを同期中…", + "scanning nearby systems...": "周辺のシェアサイクルを検索中…", + "GBFS fetch error": "GBFS取得エラー", + "Station {id}": "ステーション {id}", + "Station": "ステーション", + "🚲 {bikes} avail · {docks} docks · {capacity} cap": "🚲 貸出可 {bikes}台 · 返却可 {docks}台 · 収容 {capacity}台", + "⚠️ Not installed": "⚠️ 未設置", + "⚠️ Not renting": "⚠️ 貸出停止中", + "⚠️ Not returning": "⚠️ 返却不可", + "HDG {value}° (ESTIMATED)": "HDG {value}°(推定)", + "{count} CAMERAS STANDING BY · NO CAMERA SELECTED · CLICK A CAMERA TO ACTIVATE": "{count}台のカメラが待機中 · カメラ未選択 · カメラをクリックして有効化", + "No cameras available in catalog.": "カタログに利用できるカメラがありません。", + "{city} CCTV": "{city} CCTV", + "FOV {value}° (DATASHEET)": "FOV {value}°(データシート)", + "FOV {value}°": "FOV {value}°", + "SPEC FOV {span}° (CAPABILITY)": "仕様FOV {span}°(対応画角)", + "COVERAGE {value}km²": "カバー範囲 {value}km²", + "OVERLAP {count} cams": "重複 {count}台", + "ISOLATED VIEW": "重複なし", + "PROJ MONITOR": "投影 モニター", + "PROJ OFF": "投影 オフ", + "VIEWSHED": "可視域", + "CAL {value}": "CAL {value}", + "SRC {value}": "SRC {value}", + "{value} CONTEXT": "{value}コンテキスト", + "{spec} · SPECS: CCTV-DATABASE.COM": "{spec} · 仕様:CCTV-DATABASE.COM", + "Atlantic and eastern/central North Pacific; not worldwide cyclone coverage.": "大西洋と北太平洋東部・中部のみです。全世界のサイクロンを網羅するものではありません。", + "Cyclone advisories unavailable": "サイクロン情報を取得できません", + "Cyclone data unavailable": "サイクロンデータを取得できません", + "Potential tropical cyclone": "サイクロン化のおそれがある擾乱", + "Post-tropical cyclone": "熱帯性を失ったサイクロン", + "Hurricane": "ハリケーン", + "Tropical storm": "熱帯暴風雨", + "Tropical depression": "熱帯低気圧", + "Subtropical storm": "亜熱帯暴風雨", + "Subtropical depression": "亜熱帯低気圧", + "Track and cone match this advisory": "進路と予報円はこの情報と一致しています", + "Track/cone awaiting advisory {number}": "進路/予報円は第{number}報待ち", + "Track/cone unavailable": "進路/予報円を取得できません", + "Cached advisory · stale source": "キャッシュのサイクロン情報 · ソース更新遅延", + "Loading advisories…": "サイクロン情報を読み込み中…", + "{count} active storms": { + "other": "発生中のサイクロン {count}個" + }, + "Position as of {time}": "{time}時点の位置", + "Maximum sustained wind: {wind} · Pressure: {pressure}": "最大持続風速:{wind} · 気圧:{pressure}", + "{name} · {classification} · Advisory {number} · issued {time}": "{name} · {classification} · 第{number}報 · {time}発表", + "No active NHC/CPHC systems": "NHC/CPHCが監視中のサイクロンはありません", + "Advisories unavailable": "サイクロン情報を取得できません", + "Cyclones · NHC / CPHC": "サイクロン · NHC / CPHC", + "Atlantic · E/C Pacific": "大西洋 · 東部/中部太平洋", + "{storms} · {name} selected": "{storms} · {name}を選択中", + "Official advisory ↗": "公式情報 ↗", + "Active NHC and CPHC cyclone advisories": "発生中のNHCおよびCPHCサイクロン情報", + "Wind unavailable": "風のデータを取得できません", + "Advisory center / forecast track": "発表時の中心 / 予報進路", + "Center-track uncertainty cone": "中心進路の予報円", + "Select a storm on the map, or choose a storm in the list to select it and move the camera. Click empty map space to clear the selection. NOAA NHC/CPHC advisory context. The cone describes forecast center-track uncertainty, not storm size or the full hazard area. Forecast point labels are source lead hours, not times computed from advisory issuance. Geometry follows the surface; height is not weather altitude. Consult the official advisory.": "地図上でサイクロンを選択するか、リストから選ぶと、そのサイクロンが選択されてカメラが移動します。地図の何もない場所をクリックすると選択を解除します。NOAA NHC/CPHCの情報に基づく表示です。予報円は予報される中心進路の不確かさを示すもので、サイクロンの大きさや危険範囲全体ではありません。予報点のラベルはソースの予報時間で、発表時刻から計算した時刻ではありません。図形は地表に沿って表示され、高さは気象上の高度ではありません。公式情報を確認してください。", + "DRIVE": "車", + "Drive": "車", + "WALK": "徒歩", + "Walk": "徒歩", + "BIKE": "自転車", + "Bike": "自転車", + "rate limited": "レート制限中", + "routing service is rate limited": "ルート検索サービスがレート制限中です", + "route leg too long": "区間が長すぎます", + "route too long": "ルートが長すぎます", + "{error} — try again in a moment": "{error}。しばらくしてからもう一度お試しください", + "Routing is rate limited — try again in a moment": "ルート検索がレート制限中です。しばらくしてからもう一度お試しください", + "Routing unavailable (HTTP {status})": "ルート検索を利用できません(HTTP {status})", + "Routing unavailable": "ルート検索を利用できません", + "direction|left": "左", + "direction|right": "右", + "direction|slightly left": "斜め左", + "direction|slightly right": "斜め右", + "direction|sharply left": "急角度で左", + "direction|sharply right": "急角度で右", + "direction|straight": "まっすぐ", + "direction|around": "反対方向", + "fork direction|left": "左", + "fork direction|right": "右", + "fork direction|slightly left": "やや左", + "fork direction|slightly right": "やや右", + "fork direction|sharply left": "急角度で左", + "fork direction|sharply right": "急角度で右", + "fork direction|straight": "直進", + "lane direction|left": "左", + "lane direction|right": "右", + "lane direction|slightly left": "やや左", + "lane direction|slightly right": "やや右", + "lane direction|sharply left": "急角度で左", + "lane direction|sharply right": "急角度で右", + "lane direction|straight": "直進", + "ordinal|{number}st": "{number}番目", + "ordinal|{number}nd": "{number}番目", + "ordinal|{number}rd": "{number}番目", + "ordinal|{number}th": "{number}番目", + "Head out on {road}": "{road}から出発", + "Head out": "出発", + "Arrive at your destination, on the left": "目的地に到着(左側)", + "Arrive at your destination, on the right": "目的地に到着(右側)", + "Arrive at your destination": "目的地に到着", + "Make a U-turn onto {road}": "Uターンして{road}に入る", + "Make a U-turn": "Uターン", + "Continue straight onto {road}": "直進して{road}に入る", + "Continue straight": "直進", + "Turn {direction} onto {road}": "{direction}に曲がって{road}に入る", + "Turn {direction}": "{direction}に曲がる", + "Turn onto {road}": "曲がって{road}に入る", + "maneuver|Turn": "曲がる", + "Continue {direction} onto {road}": "{direction}に進んで{road}に入る", + "Continue {direction}": "{direction}に進む", + "At the end of the road, turn {direction} onto {road}": "突き当たりを{direction}に曲がって{road}に入る", + "At the end of the road, turn {direction}": "突き当たりを{direction}に曲がる", + "At the end of the road, continue onto {road}": "突き当たりから{road}に入る", + "At the end of the road, continue": "突き当たりをそのまま進む", + "Keep {direction} at the fork onto {road}": "分岐を{direction}方向へ進んで{road}に入る", + "Keep {direction} at the fork": "分岐を{direction}方向へ進む", + "Continue at the fork onto {road}": "分岐を進んで{road}に入る", + "Continue at the fork": "分岐をそのまま進む", + "Merge {direction} onto {road}": "{direction}に合流して{road}に入る", + "Merge {direction}": "{direction}に合流", + "Merge onto {road}": "{road}に合流", + "maneuver|Merge": "合流", + "Take the ramp on the {direction} onto {road}": "{direction}側のランプから{road}に入る", + "Take the ramp on the {direction}": "{direction}側のランプに入る", + "Take the ramp {direction} onto {road}": "ランプを{direction}に進んで{road}に入る", + "Take the ramp {direction}": "ランプを{direction}に進む", + "Take the ramp onto {road}": "ランプから{road}に入る", + "Take the ramp": "ランプに入る", + "Take the exit on the {direction} onto {road}": "{direction}側の出口から{road}に出る", + "Take the exit on the {direction}": "{direction}側の出口を出る", + "Take the exit {direction} onto {road}": "出口を{direction}に進んで{road}に出る", + "Take the exit {direction}": "出口を{direction}に出る", + "Take the exit onto {road}": "出口から{road}に出る", + "Take the exit": "出口を出る", + "At the roundabout, take the {exit} exit onto {road}": "ロータリーで{exit}の出口から{road}に出る", + "At the roundabout, take the {exit} exit": "ロータリーで{exit}の出口を出る", + "Enter the roundabout onto {road}": "ロータリーに入って{road}へ進む", + "Enter the roundabout": "ロータリーに入る", + "At the roundabout, turn {direction} onto {road}": "ロータリーで{direction}に曲がって{road}に入る", + "At the roundabout, turn {direction}": "ロータリーで{direction}に曲がる", + "At the roundabout, continue onto {road}": "ロータリーから{road}に入る", + "At the roundabout, continue": "ロータリーをそのまま進む", + "Exit the roundabout onto {road}": "ロータリーを出て{road}に入る", + "Exit the roundabout": "ロータリーを出る", + "Use the {direction} lane onto {road}": "{direction}車線を通って{road}に入る", + "Use the {direction} lane": "{direction}車線を通行", + "Continue onto {road}": "そのまま{road}に入る", + "maneuver|Continue": "そのまま進む", + "{mode} — reroute for {modeLower}": "{mode}:{modeLower}でルートを再検索", + "CLICK MAP": "地図をクリック", + "SET A": "Aを設定", + "Click a spot on the globe to place A (click again to cancel)": "地球上の地点をクリックしてAを配置(もう一度クリックでキャンセル)", + "Then click the globe to place the start": "次に地球をクリックして出発地を配置", + "SET B": "Bを設定", + "Click a spot on the globe to place B (click again to cancel)": "地球上の地点をクリックしてBを配置(もう一度クリックでキャンセル)", + "Then click the globe to place the destination": "次に地球をクリックして目的地を配置", + "Swap A and B": "AとBを入れ替え", + "FLY ···": "飛行 ···", + "FLYING": "飛行中", + "FLY": "飛行", + "Fly the camera along the route": "ルートに沿ってカメラを飛行", + "Place A and B first": "先にAとBを配置してください", + "action|CLEAR": "消去", + "Remove the route and both markers": "ルートと両方のマーカーを削除", + "press Escape twice to leave Draw": "Escを2回押して描画を終了してください", + "{name} is active — turn {name} off, then set {endpoint}": "{name}を使用中です。{name}をオフにしてから{endpoint}を設定してください", + "{name} is active — {leave}, then set {endpoint}": "{name}を使用中です。先に{leave}。その後、{endpoint}を設定してください", + "Only the first {count} turns of this route are shown": "このルートは最初の{count}件の案内のみ表示しています", + "Turn-by-turn directions": "ターンバイターンのルート案内", + "Another map tool is using clicks — close it, then SET A again": "別の地図ツールがクリックを使用中です。そのツールを閉じてから、もう一度[Aを設定]を押してください", + "Routing…": "ルート検索中…", + "No route found": "ルートが見つかりません", + "{distance} · {duration} · {mode} · first {count} turns": "{distance} · {duration} · {mode} · 最初の{count}件の案内", + "{distance} · {duration} · {mode}": "{distance} · {duration} · {mode}", + "Click the globe to place {endpoint}": "地球をクリックして{endpoint}を配置", + "SET B, then click the globe": "[Bを設定]を押してから地球をクリック", + "SET A, then click the globe": "[Aを設定]を押してから地球をクリック", + "Step {step} of {total} · then {leg}": "ステップ {step}/{total} · 次に{leg}", + "Step {step} of {total}": "ステップ {step}/{total}", + "Then: {instruction}": "次:{instruction}", + "No route found between A and B": "AとBの間にルートが見つかりません", + "Routing failed: {error}": "ルート検索に失敗しました:{error}", + "Routing timed out": "ルート検索がタイムアウトしました", + "Routing unavailable — no response from the routing service": "ルート検索を利用できません:ルート検索サービスから応答がありません", + "Earthquake source unavailable": "地震のソースを利用できません", + "Focus fire detection {title}, {details}": "火災検知{title}にフォーカス({details})", + "live feed unavailable": "ライブフィードを利用できません", + "{value} conf": "信頼度 {value}", + "{value} ago": "{value}前", + "sensor n/a": "センサー不明", + "FIRE · {frp} MW": "火災 · {frp} MW", + "NIGHT": "夜間", + "▲ {frp} MW": "▲ {frp} MW", + "FIRE": "火災", + "FIRES": "火災", + "max {value} MW": "最大 {value} MW", + "new {value}": "新規 {value}", + "{value}°N": "{value}°N", + "{value}°S": "{value}°S", + "{value}°E": "{value}°E", + "{value}°W": "{value}°W", + "just now": "たった今", + "<1m ago": "1分以内", + "{count}m ago": "{count}分前", + "{count}h ago": "{count}時間前", + "fire confidence|high": "高", + "fire confidence|nominal": "中", + "fire confidence|low": "低", + "age|<1h": "1時間以内", + "age|{count}h": "{count}時間", + "age|{count}d": "{count}日", + "STALE · cached {age}": "更新遅延 · キャッシュ {age}", + "refreshing...": "更新中…", + "loading...": "読込中…", + "KEY REQUIRED": "キーが必要", + "LIVE · updated {age}": "ライブ · 更新 {age}", + "Source snapshot time unavailable": "ソースのスナップショット時刻を取得できません", + "Live data unavailable": "ライブデータを利用できません", + "Intel Picture": "情勢図", + "Kinetic 30d": "30日間の軍事行動", + "Border Crisis": "国境危機", + "Iran Picture": "イラン情勢図", + "Iran Kinetic": "イランの軍事行動", + "Yemen FLOT": "イエメン前線(FLOT)", + "Latest Drop": "最新の公開", + "GhostMaps file could not be read": "GhostMapsファイルを読み取れませんでした", + "{count} source links ↗ · click card to open the first": { + "other": "ソースリンク{count}件 ↗ · カードをクリックで最初のリンクを開く" + }, + "S2 · {name}": "S2 · {name}", + "Report": "レポート", + "Open the source for {title}": "{title}のソースを開く", + "GhostMaps catalog unavailable": "GhostMapsカタログを利用できません", + "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": { + "other": "プレースマーク {count}件" + }, + "S2 Underground publishes these KMZ products to GitHub. New uploads are detected within seconds and replace the previous edition automatically. Click a marker for its report.": "S2 UndergroundはこれらのKMZプロダクトをGitHubで公開しています。新しいアップロードは数秒以内に検出され、以前の版と自動的に置き換わります。マーカーをクリックするとレポートを表示します。", + "Malformed GhostMaps catalog": "GhostMapsカタログの形式が不正です", + "Low-accuracy score < 2%": "低精度スコア < 2%", + "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 30 min": "地域サンプル · 半径250海里 · 30分以内に表示したビューのセル", + "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海里の円をカバーします。ズームアウトしても全世界のスナップショットにはなりません。空白のセルは、サンプリング範囲外か観測が不十分です。", + "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.": "低精度スコア = 100 × max(0, bad − 1) / total:航空機1機分を差し引くため、単純な割合ではありません。セルには{minutes}分間で{aircraft}機の航空機が必要です。航法インテグリティの異常は、妨害(ジャミング)やなりすまし(スプーフィング)を検出したことを意味しません。", + "GNSS snapshot needs a view anchor": "GNSSスナップショットにはビューのアンカーが必要です", + "GNSS feed rate limited (HTTP 429)": "GNSSフィードがレート制限中です(HTTP 429)", + "Malformed GNSS integrity snapshot": "GNSSインテグリティのスナップショットの形式が不正です", + "GNSS integrity snapshot has no observation time": "GNSSインテグリティのスナップショットに観測時刻がありません", + "installation class|AIRFIELD": "飛行場", + "installation class|NAVAL BASE": "海軍基地", + "installation class|RANGE": "射撃場", + "installation class|MILITARY LAND": "軍用地", + "installation class|BASE": "基地", + "installation class|BARRACKS": "兵舎", + "installation class|TRAINING AREA": "訓練区域", + "installation class|PLACES CANDIDATE": "Places候補", + "installation class|INSTALLATION": "施設", + "No mapped sites within {km} km of the contact": "コンタクトから{km} km以内にマップ済み施設はありません", + "{count} mapped sites within {km} km of the contact": { + "other": "コンタクトから{km} km以内のマップ済み施設 {count}か所" + }, + "No mapped sites in view": "表示範囲内にマップ済み施設はありません", + "{count} mapped sites in view": { + "other": "表示範囲内のマップ済み施設 {count}か所" + }, + "loading mapped installation context": "マップ済み施設のコンテキストを読み込み中", + "Too many mapped sites in view to list them all": "表示範囲内のマップ済み施設が多すぎて、すべてを一覧表示できません", + "Installation context unavailable": "施設の情報を取得できません", + "MAPPED INSTALLATION": "マップ済み施設", + "Unknown": "不明", + "LAUNCH SITE · {site}": "射場 · {site}", + "LAUNCH SITE": "射場", + "T−{time} · {mission}": "T−{time} · {mission}", + "LAUNCH STANDBY": "打ち上げ待機", + "LIFTOFF · {mission}": "リフトオフ · {mission}", + "ASCENT REPLAY · {mission}": "上昇リプレイ · {mission}", + "ASCENT ESTIMATE · {mission}": "上昇推定 · {mission}", + "STAGE RE-ENTRY / RECOVERY · {mission}": "段の再突入 / 回収 · {mission}", + "ORBIT REPLAY · {mission}": "軌道リプレイ · {mission}", + "PAUSED · {title}": "一時停止 · {title}", + "UNSPECIFIED": "未指定", + "+{count} additional payload records": "追加のペイロード記録 +{count}件", + "PAYLOAD DATA UNAVAILABLE": "ペイロードデータなし", + "{value} KM DOWNRANGE": "ダウンレンジ {value} km", + "POSITION UNAVAILABLE": "位置を取得できません", + "FLIGHT {count}": "{count}回目の飛行", + "REUSED": "再使用", + "NO STAGE RE-ENTRY / RECOVERY DATA": "段の再突入 / 回収データなし", + "{count} / 30D": "{count} / 30日", + "NO MISSIONS AVAILABLE IN THE CURRENT 30-DAY WINDOW": "現在の30日間に該当するミッションはありません", + "DATE UNAVAILABLE": "日付不明", + "UNSPECIFIED OPERATOR": "運用者未指定", + "Select {mission}": "{mission}を選択", + "{value} KM": "{value} km", + "{value} KM/S": "{value} km/s", + "Selected Space Mission": "選択中の宇宙ミッション", + "SELECTED SPACE MISSION": "選択中の宇宙ミッション", + "Show all missions": "すべてのミッションを表示", + "Deselect mission": "ミッションの選択を解除", + "MISSION": "ミッション", + "STATUS · ": "ステータス · ", + "LAUNCH SITE · ": "射場 · ", + "LAUNCH TIME · ": "打ち上げ時刻 · ", + "ORBIT · ": "軌道 · ", + "ASCENT PATH · ": "上昇経路 · ", + "CURRENT DISTANCE FROM EARTH · ": "現在の地球からの距離 · ", + "SATELLITE SPEED · ": "衛星速度 · ", + "PAYLOAD": "ペイロード", + "NAME": "名称", + "TYPE": "種別", + "DESTINATION": "目的地", + "STAGE / RE-ENTRY / RECOVERY": "段 / 再突入 / 回収", + "STAGE": "段", + "STATUS": "ステータス", + "FINAL POSITION": "最終位置", + "REPLAY SPEED": "リプレイ速度", + "Replay speed multiplier": "リプレイ速度の倍率", + "REPLAY ASCENT": "上昇をリプレイ", + "Previous mission": "前のミッション", + "PREV": "前へ", + "Next mission": "次のミッション", + "SHOW ALL / DESELECT": "すべて表示 / 選択解除", + "Pause replay": "リプレイを一時停止", + "Cancel replay": "リプレイを中止", + "EST. DOWNRANGE": "推定ダウンレンジ", + "PLANNED · ": "計画 · ", + "SUPPLIED TRAJECTORY POINTS": "提供された軌道点", + "RECONSTRUCTED ESTIMATE": "再構成による推定", + "Replay the estimated ascent with a following camera": "追従カメラで、推定された上昇をリプレイします", + "Resume replay": "リプレイを再開", + "T minus {seconds}": "打ち上げ{seconds}秒前", + "Preparing launch site": "射場を準備中", + "Liftoff": "リフトオフ", + "Ascent replay": "上昇リプレイ", + "Orbit replay": "軌道リプレイ", + ", paused": "、一時停止中", + "Malformed launch snapshot": "打ち上げスナップショットの形式が不正です", + "Heard by your receiver": "受信機で受信", + "1090 MHz": "1090 MHz", + "978 MHz UAT": "978 MHz UAT", + "browser SDR": "ブラウザーSDR", + "decoder feed": "デコーダーフィード", + "Your RTL-SDR receiver and decoder feed": "お使いのRTL-SDR受信機とデコーダーフィード", + "Your decoder feed": "お使いのデコーダーフィード", + "Your RTL-SDR receiver": "お使いのRTL-SDR受信機", + "NO CALLSIGN": "コールサインなし", + "ALT {altitude} FT · GS {speed} KT · TRK {track}": "高度 {altitude} ft · 対地速度 {speed} kt · 対地針路 {track}", + "V/S {rate}": "昇降率 {rate}", + "V/S {rate} FPM": "昇降率 {rate} ft/min", + "POSITION {age}": "位置 {age}", + "POSITION {seconds} S AGO": "位置 {seconds}秒前", + "{count} MSGS": { + "other": "メッセージ {count}件" + }, + "Decoder feeds": "デコーダーフィード", + "WebUSB + decoder feeds": "WebUSB + デコーダーフィード", + "opening receiver": "受信機を開いています", + "checking decoder feeds": "デコーダーフィードを確認中", + "WebUSB needs desktop Chrome or Edge": "WebUSBにはデスクトップ版ChromeまたはEdgeが必要です", + "connect a receiver in Radio": "ラジオで受信機を接続してください", + "receiver is in FM mode": "受信機はFMモードです", + "listening": "受信中", + "USB error": "USBエラー", + "Alt unknown": "高度不明", + "Type unknown": "機種不明", + "Reg unknown": "登録記号不明", + "Operator unknown": "運航者不明", + "${amount}K": "${amount}K", + "${amount}M": "${amount}M", + "${amount}B": "${amount}B", + "fire cause|Human": "人為", + "fire cause|Natural": "自然", + "fire cause|Undetermined": "未確定", + "fire cause|Unknown": "不明", + "fire behavior|Minimal": "微弱", + "fire behavior|Moderate": "中程度", + "fire behavior|Active": "活発", + "fire behavior|Extreme": "極めて活発", + "Type {level} Incident": "タイプ{level}インシデント", + "age|{count}m": "{count}分", + "{acres} ac": { + "other": "{acres}エーカー" + }, + "{percent}% contained": "封じ込め率 {percent}%", + "containment unknown": "封じ込め率不明", + "{cause} cause": "原因:{cause}", + "{personnel} personnel": { + "other": "人員{personnel}人" + }, + "{county} County": "{county}郡", + "{cost} to date": "これまでの費用 {cost}", + "discovered {age} ago": "{age}前に発見", + "updated {age} ago": "{age}前に更新", + "part of {complex}": "{complex}の一部", + "InciWeb ↗ · click card to open": "InciWeb ↗ · カードをクリックで開く", + "FIRE · {name}": "火災 · {name}", + "Unnamed incident": "名称未設定の火災", + "Open {title} on InciWeb": "InciWebで{title}を開く", + "Perimeter source unavailable": "延焼範囲のソースを利用できません", + "Not contained or unknown": "封じ込め率0%または不明", + "Under 50% contained": "封じ込め率50%未満", + "50–99% contained": "封じ込め率50–99%", + "Fully contained": "封じ込め率100%", + "Colour shows reported containment. Perimeters are simplified to about 100 m.": "色は報告された封じ込め率を示します。延焼範囲の輪郭は約100 mの精度で簡略化されています。", + "Malformed perimeter snapshot": "延焼範囲スナップショットの形式が不正です", + "radio tag|All": "すべて", + "News": "ニュース", + "Talk": "トーク", + "Weather / Emergency": "気象 / 緊急", + "Public Safety": "警察・消防", + "Aviation / Marine": "航空 / 海事", + "Traffic / Transit": "交通 / 公共交通", + "Music": "音楽", + "Other": "その他", + "Directory refresh degraded; showing the previous station catalog.": "ディレクトリの更新が不完全です。前回の局カタログを表示しています。", + "Radio directory coverage is degraded.": "ラジオディレクトリのカバー範囲が不完全です。", + "Directory refresh failed; showing the previous station catalog.": "ディレクトリの更新に失敗しました。前回の局カタログを表示しています。", + "Radio directory is temporarily unavailable.": "ラジオディレクトリは一時的に利用できません。", + "radio|NEWS": "ニュース", + "radio|TALK": "トーク", + "radio|WEATHER": "気象", + "radio|SAFETY": "警察・消防", + "radio|AIR / SEA": "航空 / 海事", + "radio|TRANSIT": "交通", + "radio|MUSIC": "音楽", + "radio|OTHER": "その他", + "Broadcaster stream is unavailable or blocked by the browser.": "放送局のストリームを利用できないか、ブラウザーにブロックされています。", + "Playback requires a direct click or tap.": "再生するには直接クリックまたはタップしてください。", + "Broadcaster stream could not be started.": "放送局のストリームを開始できませんでした。", + "Alternative": "オルタナティブ", + "Ambient": "アンビエント", + "Blues": "ブルース", + "Classical": "クラシック", + "Country": "カントリー", + "Dance": "ダンス", + "Electronic": "エレクトロニック", + "Folk": "フォーク", + "Funk": "ファンク", + "Hip-Hop": "ヒップホップ", + "House": "ハウス", + "Indie": "インディー", + "Jazz": "ジャズ", + "Latin": "ラテン", + "Metal": "メタル", + "Oldies": "オールディーズ", + "Pop": "ポップ", + "Punk": "パンク", + "R&B": "R&B", + "Reggae": "レゲエ", + "Rock": "ロック", + "Soul": "ソウル", + "Techno": "テクノ", + "Trance": "トランス", + "World": "ワールド", + "overview": "概観", + "cloud unknown": "雲量不明", + "{percent}% scene cloud": "シーン雲量 {percent}%", + "{min}–{max}% scene cloud": "シーン雲量 {min}–{max}%", + "Comparison in use by another scene": "比較機能は別のシーンで使用中です", + "Esri map unavailable · no swipe": "Esriマップ利用不可 · スワイプ比較不可", + "Swipe needs a globe map": "スワイプ比較には地球儀マップが必要です", + "Imagery stays on Esri · CLEAR to use Google 3D": "画像はEsriのまま · [消去]でGoogle 3Dを使用", + "Imagery catalog unavailable": "画像カタログを利用できません", + "Catalog truncated · a newer clear day may exist": "カタログ省略あり · より新しい雲の少ない日がある可能性", + "{label} catalog unavailable · {detail}": "{label}のカタログを利用できません · {detail}", + "Daily overview for today may still be empty until the pass": "今日の日次概観は、衛星の通過まで空の場合があります", + "Box and images cleared": "範囲と画像を消去しました", + "MORE DETAIL · 30 m": "高精細 · 30 m", + "Sentinel-2 and Landsat via HLS, 30 m, every 2–6 days": "HLS経由のSentinel-2とLandsat、30 m、2–6日ごと", + "DAILY OVERVIEW · 250 m": "日次概観 · 250 m", + "VIIRS daily mosaic, 250 m, follows broad changes; little detail under 25 km": "VIIRSの日次モザイク、250 m。広域の変化を追跡。25 km未満の範囲では細部をほぼ判別できない", + "Sentinel-2": "Sentinel-2", + "Sentinel-2 via HLS": "Sentinel-2(HLS経由)", + "Landsat 8/9": "Landsat 8/9", + "Landsat 8/9 via HLS": "Landsat 8/9(HLS経由)", + "Daily overview": "日次概観", + "Box edges must be finite": "範囲の端は有限の値である必要があります", + "Longitudes must be within ±180°": "経度は±180°以内である必要があります", + "Select one side of the dateline": "日付変更線の片側を選択してください", + "Box must enclose an area": "幅と高さのある範囲を選択してください", + "Add the full Starlink broadband shell (thousands of extra points)": "Starlinkブロードバンドシェル全体を追加(数千の追加ポイント)", + "Loading the Starlink shell…": "Starlinkシェルを読み込み中…", + "Starlink {detail} — click to retry": "Starlink {detail}:クリックで再試行", + "load failed": "読込失敗", + "Showing the full Starlink shell — click for the core catalog only": "Starlinkシェル全体を表示中:クリックでコアカタログのみに戻す", + "DENSE ···": "高密度 ···", + "DENSE ✕": "高密度 ✕", + "DENSE": "高密度", + "CelesTrak unreachable": "CelesTrakに接続できません", + "DOCKED · {name} · +{extra}": "ドッキング中 · {name} · +{extra}", + "DOCKED · {name}": "ドッキング中 · {name}", + "{name} needs the Google 3D map: choose it under MAP SOURCE": "{name}にはGoogle 3Dマップが必要です。マップソースで選択してください", + "zoom in to a street to open {name}": "{name}を開くには通りまでズームイン", + "loading coverage...": "カバー範囲を読み込み中…", + "No street-level imagery within {radius} m of that point": "その地点から{radius} m以内にストリートレベル画像はありません", + "No street-level imagery within {radius} m of the view centre": "表示中心から{radius} m以内にストリートレベル画像はありません", + "Zoom in to see street-level coverage": "ズームインしてストリートレベルのカバー範囲を表示", + "Point the camera at the ground for street-level coverage": "ストリートレベルのカバー範囲を見るには、カメラを地面に向けてください", + "Coverage tile failed": "カバー範囲タイルの読み込みに失敗しました", + "The photo could not be loaded": "写真を読み込めませんでした", + "{name} photo. ← and → step along the sequence. On a 360° photo, drag or use Shift+← →, ↑ ↓ to look around, and + − to zoom": "{name}の写真。←と→でシーケンス内を移動。360°写真では、ドラッグまたはShift+← →、↑ ↓で見回し、+ −でズーム", + "{name} street-level photo": "{name}のストリートレベル写真", + "Previous photo in this sequence (←)": "このシーケンスの前の写真(←)", + "Next photo in this sequence (→)": "このシーケンスの次の写真(→)", + "Aim the middle of the view at a street to look for imagery there": "画像を探すには、表示の中央を通りに合わせてください", + "KEY REJECTED": "キーが拒否されました", + "GOOGLE_MAPS_API_KEY is not set": "GOOGLE_MAPS_API_KEYが設定されていません", + "The Google Maps JavaScript API could not load": "Google Maps JavaScript APIを読み込めませんでした", + "Google Street View": "Googleストリートビュー", + "GOOGLE STREET VIEW": "Googleストリートビュー", + "Google refused GOOGLE_MAPS_API_KEY for Street View — check that the Maps JavaScript API is enabled for it and this site is an allowed referrer": "GoogleがストリートビューへのGOOGLE_MAPS_API_KEYを拒否しました。このキーでMaps JavaScript APIが有効になっていること、このサイトが許可されたリファラーであることを確認してください", + "Street View did not answer — check the network, and that the Maps JavaScript API is enabled for GOOGLE_MAPS_API_KEY": "ストリートビューから応答がありません。ネットワーク、およびGOOGLE_MAPS_API_KEYでMaps JavaScript APIが有効になっていることを確認してください", + "click a street to open Street View": "通りをクリックしてストリートビューを開く", + "Street View has no imagery for this place": "この場所のストリートビュー画像はありません", + "KartaView has no position for this photo": "KartaViewにこの写真の位置情報がありません", + "The KartaView photo could not be loaded": "KartaViewの写真を読み込めませんでした", + "zoom in to see KartaView coverage": "ズームインしてKartaViewのカバー範囲を表示", + "no KartaView photos in view": "表示範囲内にKartaViewの写真はありません", + "KartaView is rate-limiting requests — they resume on their own": "KartaViewがリクエストをレート制限中です。自動的に再開されます", + "KartaView’s photos here are archived and cannot be shown": "この場所のKartaViewの写真はアーカイブ済みのため表示できません", + "KartaView did not filter this area in time — move the map to ask again": "KartaViewがこの範囲を時間内に絞り込めませんでした。地図を動かして再度リクエストしてください", + "None of KartaView’s photos here pass the imagery filter": "この場所のKartaViewの写真はいずれも画像フィルターを通過しません", + "KartaView did not answer in time — try again": "KartaViewから時間内に応答がありませんでした。もう一度お試しください", + "KartaView could not be reached — check the network": "KartaViewに接続できません。ネットワークを確認してください", + "KartaView sent an answer that could not be read": "KartaViewから読み取れない応答が返されました", + "KartaView is rate-limiting requests": "KartaViewがリクエストをレート制限中です", + "KartaView sent too much data": "KartaViewから送られたデータが多すぎます", + "KartaView is rate-limiting requests — try again in a moment": "KartaViewがリクエストをレート制限中です。しばらくしてからもう一度お試しください", + "KartaView has no such photo": "KartaViewにその写真はありません", + "Mapillary rejected MAPILLARY_CLIENT_TOKEN — replace it in Provider Settings": "MapillaryがMAPILLARY_CLIENT_TOKENを拒否しました。プロバイダー設定で置き換えてください", + "Coverage requests are being rate-limited — they resume on their own": "カバー範囲のリクエストがレート制限中です。自動的に再開されます", + "The Panoramax photo could not be loaded": "Panoramaxの写真を読み込めませんでした", + "Panoramax is busy — coverage resumes on its own": "Panoramaxが混雑しています。カバー範囲の取得は自動的に再開されます", + "no Panoramax photos in view": "表示範囲内にPanoramaxの写真はありません", + "no Panoramax photos here match the filters": "この場所にフィルターに一致するPanoramaxの写真はありません", + "Panoramax sent a map tile that could not be read": "Panoramaxから読み取れない地図タイルが返されました", + "This Panoramax photo is no longer available": "このPanoramaxの写真は利用できなくなりました", + "zoom in to see Panoramax coverage": "ズームインしてPanoramaxのカバー範囲を表示", + "Panoramax did not answer in time — try again": "Panoramaxから時間内に応答がありませんでした。もう一度お試しください", + "Panoramax could not be reached — check the connection": "Panoramaxに接続できません。接続を確認してください", + "Panoramax sent too much data": "Panoramaxから送られたデータが多すぎます", + "Panoramax is busy": "Panoramaxが混雑しています", + "Panoramax sent an unreadable answer": "Panoramaxから読み取れない応答が返されました", + "Sequence images unavailable": "シーケンス画像を利用できません", + "No street-level imagery providers configured": "ストリートレベル画像のプロバイダーが設定されていません", + "(by upload date)": "(アップロード日基準)", + "Open the Street Level panel to view imagery": "画像を表示するには、ストリートレベルパネルを開いてください", + "Image could not be opened": "画像を開けませんでした", + "TeleGeography load failed": "TeleGeographyの読み込みに失敗しました", + "UNAVAILABLE · {source} · Roads unavailable": "利用不可 · {source} · 道路を利用できません", + "Roads: {source}": "道路:{source}", + "TomTom roads unavailable while the traffic service is unreachable": "交通サービスに接続できない間はTomTomの道路を利用できません", + "Hybrid unavailable while the traffic service is unreachable": "交通サービスに接続できない間はハイブリッドを利用できません", + "TomTom roads need a TomTom key": "TomTomの道路にはTomTomキーが必要です", + "Hybrid needs a TomTom key": "ハイブリッドにはTomTomキーが必要です", + "Local surface still loading": "周辺の道路を読み込み中", + "Partial coverage": "一部のみカバー", + "Detailed roads unavailable": "詳細な道路を利用できません", + "Reduced detail coverage": "詳細データのカバー範囲が限定的", + "TomTom roads only, each with its live flow": "TomTomの道路のみ(各道路にライブの交通流)", + "TomTom roads only, each with its live flow (needs a TomTom key)": "TomTomの道路のみ(各道路にライブの交通流、TomTomキーが必要)", + "OpenStreetMap roads, with TomTom flow matched when available": "OpenStreetMapの道路(利用可能な場合はTomTomの交通流を照合)", + "TomTom roads first; OpenStreetMap fills the rest, simulated": "TomTomの道路を優先し、残りはOpenStreetMapで補完(シミュレーション)", + "TomTom roads first; OpenStreetMap fills the rest, simulated (needs a TomTom key)": "TomTomの道路を優先し、残りはOpenStreetMapで補完(シミュレーション、TomTomキーが必要)", + "Hybrid": "ハイブリッド", + "TomTom key unavailable": "TomTomキーを利用できません", + "TomTom daily budget reached": "TomTomの1日の上限に達しました", + "TomTom upstream unreachable": "TomTomのサービスに接続できません", + "TomTom flow unavailable": "TomTomの交通流を利用できません", + "SIMULATED — {reason}": "シミュレーション:{reason}", + "Syncing flow · Roads: {source}": "交通流を同期中 · 道路:{source}", + "LIVE · Roads: {source} · Roads without flow hidden": "ライブ · 道路:{source} · 交通流のない道路は非表示", + "LIVE · Roads: {source} · No flow roads in view": "ライブ · 道路:{source} · 表示範囲に交通流のある道路なし", + "LIVE · Roads: {source} · Flow {percent}%": "ライブ · 道路:{source} · 交通流 {percent}%", + "SIMULATED · Roads: {source} · Flow {percent}%": "シミュレーション · 道路:{source} · 交通流 {percent}%", + "STALE · Roads: {source} · Flow: cached TomTom tiles": "更新遅延 · 道路:{source} · 交通流:キャッシュのTomTomタイル", + "Syncing flow · Roads: OpenStreetMap · Flow: TomTom · Unmatched: simulated": "交通流を同期中 · 道路:OpenStreetMap · 交通流:TomTom · 未一致:シミュレーション", + "LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov · Unmatched: simulated": "ライブ · 道路:OpenStreetMap · 交通流:TomTom · カバー率 {percent}% · 未一致:シミュレーション", + "LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov": "ライブ · 道路:OpenStreetMap · 交通流:TomTom · カバー率 {percent}%", + "SIMULATED · Roads: OpenStreetMap · Flow: TomTom (no matches)": "シミュレーション · 道路:OpenStreetMap · 交通流:TomTom(一致なし)", + "SIMULATED — traffic service unreachable": "シミュレーション:交通サービスに接続できません", + "SIMULATED — add TomTom key for live": "シミュレーション:ライブ表示にはTomTomキーを追加", + "OpenFreeMap tiles rate-limited": "OpenFreeMapタイルがレート制限中", + "OpenFreeMap tiles timed out": "OpenFreeMapタイルがタイムアウトしました", + "OpenFreeMap tiles unavailable": "OpenFreeMapタイルを利用できません", + "occupancy|empty": "乗客なし", + "occupancy|many seats available": "空席多数", + "occupancy|few seats available": "空席わずか", + "occupancy|standing room only": "立ち乗りのみ", + "occupancy|crushed standing room only": "非常に混雑", + "occupancy|full": "満員", + "occupancy|not accepting passengers": "乗車不可", + "occupancy|not boardable": "乗車できません", + "Route {route}": "路線 {route}", + "Vehicle {vehicle}": "車両 {vehicle}", + "vehicle motion|Stopped": "停車中", + "No recent fix": "最近の測位なし", + "Waiting for update": "更新待ち", + "Moving": "走行中", + "reported {speed} km/h": "報告速度 {speed} km/h", + "reported hdg {heading}°": "報告方位 {heading}°", + "Last report: stopped at stop {stop}": "最終報告:停留所{stop}に停車中", + "Last report: arriving at stop {stop}": "最終報告:停留所{stop}に接近中", + "Last report: next stop {stop}": "最終報告:次の停留所{stop}", + "Shown at street level · depth not in the feed": "地表の高さで表示 · フィードに深さ情報なし", + "Reported {seconds} s ago": "{seconds}秒前に報告", + "Reported {minutes} min ago": "{minutes}分前に報告", + "shown {seconds} s behind": "{seconds}秒遅れで表示", + "{reported} (feed time)": "{reported}(フィード時刻)", + "Available trail: {minutes} min (limited)": "利用可能な軌跡:{minutes}分(制限あり)", + "Available trail: {minutes} min": "利用可能な軌跡:{minutes}分", + "Bus": "バス", + "Tram": "路面電車", + "Subway": "地下鉄", + "Train": "鉄道", + "Ferry": "フェリー", + "Transit vehicle": "公共交通車両", + "AIS: {name}": "AIS: {name}", + "{type} SPD: {speed} HDG: {heading}": "{type} SPD: {speed} HDG: {heading}", + "VESSEL": "船舶", + "{speed}KT": "{speed} kt", + "{heading}DEG": "{heading}°", + "POS: LIVE": "POS: ライブ", + "POS: {time}Z": "POS: {time}Z", + "AIS live load failed": "AISライブデータの読み込みに失敗しました", + "awaiting first AIS position…": "最初のAIS位置情報待ち…", + "awaiting usable AIS positions…": "利用可能なAIS位置情報待ち…", + "awaiting first AIS message…": "最初のAISメッセージ待ち…", + "AISSTREAM_API_KEY not set": "AISSTREAM_API_KEYが設定されていません", + "live feed unsupported": "ライブフィード非対応", + "connecting to feed…": "フィードに接続中…", + "feed disconnected": "フィード切断", + "feed down": "フィード停止", + "feed idle": "フィード待機中", + "API key rejected — check AISSTREAM_API_KEY": "APIキーが拒否されました。AISSTREAM_API_KEYを確認してください", + "feed silent {seconds}s — no AIS data": "フィード無応答 {seconds}秒:AISデータなし", + "feed silent — no AIS data": "フィード無応答:AISデータなし", + "feed down — retrying slowly (attempt {attempt})": "フィード停止:低頻度で再試行中({attempt}回目)", + "reconnecting to feed… (attempt {attempt})": "フィードに再接続中…({attempt}回目)", + "feed down — retrying slowly": "フィード停止:低頻度で再試行中", + "reconnecting to feed…": "フィードに再接続中…", + "Focus vessel {name}, MMSI {mmsi}": "船舶{name}(MMSI {mmsi})にフォーカス", + "Hidden by this map source · choose a globe map": "このマップソースでは非表示 · 地球儀マップを選択してください", + "Weather image HTTP {status}": "気象画像 HTTP {status}", + "Weather HTTP {status}": "気象 HTTP {status}", + "Frame unavailable; previous observation retained": "フレームを利用できません。前回の観測を保持しています", + "Weather imagery unavailable": "気象画像を利用できません", + "Weather source unavailable; previous observation retained": "気象ソースを利用できません。前回の観測を保持しています", + "Weather unavailable": "気象データを利用できません", + "No frame within {gap} min of {time}": "{time}の前後{gap}分以内にフレームがありません", + "No frame within {gap} h of {time}": "{time}の前後{gap}時間以内にフレームがありません", + "{hours}h {minutes}m ago": "{hours}時間{minutes}分前", + "{minutes}m ago": "{minutes}分前", + "{state} · {time} · {lag}": "{state} · {time} · {lag}", + "Observed": "観測", + "History": "履歴", + "Observation unavailable": "観測を利用できません", + "Waiting for observation": "観測データ待ち", + "{detail} · synced": "{detail} · 同期済み", + "{detail} · nearest": "{detail} · 最も近いフレーム", + "RADAR REFLECTIVITY · dBZ": "レーダー反射強度 · dBZ", + "LIGHTNING DENSITY · 15 min accumulation": "雷密度 · 15分間積算", + "GLOBAL INFRARED · hourly": "全球赤外 · 1時間ごと", + "GOES INFRARED · ~5 min": "GOES赤外 · 約5分ごと", + "{state}: {time}": "{state}:{time}", + "Latest observation": "最新の観測", + "frame {index}/{total}": "フレーム {index}/{total}", + "loading": "読み込み中", + "Observation: unavailable": "観測:利用不可", + "Contiguous US · gaps ≠ no rain": "米国本土 · 欠測 ≠ 降水なし", + "Americas + Pacific · not individual strikes": "南北アメリカ + 太平洋 · 個々の落雷ではありません", + "Color: strikes/km²/min ×10³": "色:落雷数/km²/分 ×10³", + "60°S–60°N · typically 2–3 h delayed": "60°S–60°N · 通常2–3時間遅れ", + "North America · infrared imagery": "北米 · 赤外画像", + "Reduced motion · history playback unavailable": "動きを減らす設定 · 履歴再生は利用不可", + "Reduced motion · manual history available": "動きを減らす設定 · 履歴は手動で表示可能", + "Rain radar · US": "降水レーダー · 米国", + "Lightning density · 15 min": "雷密度 · 15分", + "Satellite clouds": "衛星雲画像", + "Americas + Pacific": "南北アメリカ + 太平洋", + "Global · 60°S–60°N": "全球 · 60°S–60°N", + "North America": "北米", + "Source observations delayed": "ソースの観測が遅れています", + "Stale source": "ソース更新遅延", + "Loading next frame…": "次のフレームを読み込み中…", + "Map center outside coverage": "地図の中心がカバー範囲外です", + "strikes/km²/min ×10³": "落雷数/km²/分 ×10³", + "N. America": "北米", + "Global": "全球", + "Hourly global mosaic; usually 2–3 hours delayed": "1時間ごとの全球モザイク。通常2–3時間遅れ", + "GOES regional clouds; approximately 5-minute updates": "GOESの地域雲画像。約5分ごとに更新", + "Clouds only": "雲のみ", + "Apply the full cloud brightness key; 100% filter strength": "雲の輝度キーを完全に適用。フィルター強度100%", + "Full": "全体", + "Show the complete infrared image; 0% filter strength": "赤外画像全体を表示。フィルター強度0%", + "Soft": "弱", + "Vivid": "強", + "Image opacity; does not alter the observed values": "画像の不透明度。観測値は変わりません", + "View US radar": "米国のレーダーを表示", + "View Americas & Pacific": "南北アメリカと太平洋を表示", + "View coverage": "カバー範囲を表示", + "{value} strikes/km²/min ×10³ (15-minute density)": "落雷数 {value}/km²/分 ×10³(15分間の密度)", + "{value} dBZ radar reflectivity": "レーダー反射強度 {value} dBZ", + "STALE · cached source metadata": "更新遅延 · キャッシュのソースメタデータ", + "Map center is outside source coverage": "地図の中心がソースのカバー範囲外です", + "NOAA/NWS 15-minute lightning density derived from Vaisala NLDN/GLD360. Coverage 110°E across the Pacific/Americas to 0°, 25°S–80°N. Not a live strike count, global coverage or a safety warning.": "Vaisala NLDN/GLD360から算出したNOAA/NWSの15分間雷密度。カバー範囲は東経110°から太平洋・南北アメリカを経て0°まで、南緯25°–北緯80°。リアルタイムの落雷数、全球カバー、安全上の警報ではありません。", + "NOAA MRMS radar echoes indicate precipitation patterns, not rain rate, a storm warning or a future forecast. Native source approximately 1 km; display is limited to level 6. Frames use exact advertised observation times.": "NOAA MRMSのレーダーエコーは降水のパターンを示すもので、降水強度、嵐の警報、将来の予報ではありません。元データの解像度は約1 km、表示はズームレベル6までです。各フレームは公表された正確な観測時刻を使用します。", + "GOES-19/18 longwave infrared Band 14 regional; NESDIS global longwave mosaic. Clouds only dims everything but bright, cold cloud tops; a brightness filter, not a cloud mask. Coverage and freshness differ by region.": "GOES-19/18長波赤外バンド14の地域画像と、NESDISの全球長波モザイク。「雲のみ」は明るく冷たい雲頂以外をすべて暗くします。これは輝度フィルターであり、雲マスクではありません。カバー範囲と鮮度は地域によって異なります。", + "REGION": "地域", + "IMAGE": "画像", + "FILTER": "フィルター", + "Cloud filter strength": "雲フィルターの強度", + "OPACITY": "不透明度", + "Infrared decode failed": "赤外画像のデコードに失敗しました", + "Weather prefetch unavailable": "気象のプリフェッチを利用できません", + "Weather tiles unavailable · previous frame retained": "気象タイルを利用できません · 前のフレームを保持", + "Some weather tiles unavailable": "一部の気象タイルを利用できません", + "Malformed weather manifest": "気象マニフェストの形式が不正です", + "Weather request timed out": "気象リクエストがタイムアウトしました", + "Temperature field unavailable": "気温場を利用できません", + "Mean sea level pressure field unavailable": "平均海面気圧場を利用できません", + "Wind upstream unavailable": "風データの配信元を利用できません", + "{source} HTTP {status}": "{source} HTTP {status}", + "Wind": "風", + "Wind source unavailable": "風のソースを利用できません", + "Forecast · does not follow history": "予報 · 履歴とは連動しません", + "Wind motion": "風の流れ", + "Wind speed": "風速", + "Sea-level pressure": "海面気圧", + "Air temperature · 2 m": "気温 · 2 m", + "Global · 1° grid": "全球 · 1°格子", + "{model} forecast · {time}": "{model}予報 · {time}", + "Loading forecast": "予報を読み込み中", + "Preparing flow": "流れを準備中", + "Selected field unavailable": "選択した要素を利用できません", + "Cached forecast · stale": "キャッシュの予報 · 更新遅延", + "ECMWF IFS surface forecast": "ECMWF IFS地上予報", + "NOAA GFS surface forecast": "NOAA GFS地上予報", + "Reduced motion": "動きを減らす", + "Resume": "再開", + "Pause": "一時停止", + "Pause visual flow; forecast time does not advance with animation": "流れのアニメーションを一時停止。予報時刻はアニメーションでは進みません", + "None": "なし", + "Speed": "風速", + "Pressure": "気圧", + "Temperature": "気温", + "Wind trails with no field shading": "要素の塗り分けなしで風の軌跡を表示", + "How hard the surface wind is blowing": "地上風の強さ", + "Sea-level air pressure: broad highs and lows": "海面気圧:広域の高気圧と低気圧", + "Air temperature two meters above the surface": "地上2 mの気温", + "Wind speed units": "風速の単位", + "Read wind at map center": "地図中心の風を読み取る", + "Read the forecast at the center of the map without changing selection": "選択を変えずに地図中心の予報値を読み取る", + "{model} forecast · {field} ({unit})": "{model}予報 · {field}({unit})", + "Valid: {time}": "対象時刻:{time}", + "preparing": "準備中", + "Issued: {time}": "発表:{time}", + "Selected field unavailable · wind remains visible": "選択した要素を利用できません · 風は表示されたまま", + "{error} · wind remains visible": "{error} · 風は表示されたまま", + "Surface wind at 10 m. Approximately 1° global grid. Curves follow the 10 m wind field, lifted 12 km for visibility; display height is not weather altitude. View lighting is for readability. Animation shows flow through one fixed forecast; it does not advance time. Color fields drape the globe basemap or the active photorealistic 3D Tiles.": "高度10 mの地上風。約1°の全球格子。曲線は10 mの風の場に沿っており、見やすさのため12 km持ち上げています。表示の高さは気象上の高度ではありません。ビューの照明は読みやすさのためです。アニメーションは1つの固定された予報内の流れを示し、時間は進みません。色の塗り分けは地球儀のベースマップまたは有効なフォトリアル3D Tilesの上に重なります。", + "MODEL": "モデル", + "FIELD": "要素", + "UNITS": "単位", + "MOTION": "モーション", + "Forecast": "予報", + "N": "北", + "NNE": "北北東", + "NE": "北東", + "ENE": "東北東", + "E": "東", + "ESE": "東南東", + "SE": "南東", + "SSE": "南南東", + "S": "南", + "SSW": "南南西", + "SW": "南西", + "WSW": "西南西", + "W": "西", + "WNW": "西北西", + "NW": "北西", + "NNW": "北北西", + "No surface reading": "地上の値がありません", + "Aim the map center at Earth": "地図の中心を地球に合わせてください", + "A location reading needs a loaded forecast and the Earth at the center of the view.": "地点の値を読み取るには、予報が読み込まれ、表示の中心に地球がある必要があります。", + "Interpolated model forecast on an approximately 1° grid. Broad weather patterns, not a street-level measurement.": "約1°格子で補間したモデル予報です。大まかな気象パターンを示すもので、街区単位の実測値ではありません。", + " · calm": " · 静穏", + " from {direction}": " · {direction}の風", + "WIND AT {coordinates}": "{coordinates}の風", + "{model} · valid {time}": "{model} · 対象時刻 {time}", + "Wind speed field unavailable": "風速場を利用できません", + "Globe imagery unavailable": "地球儀の画像を利用できません", + "Globe field image unavailable": "地球儀の要素画像を利用できません", + "Unknown wind model": "不明な風モデル", + "Unknown weather overlay": "不明な気象オーバーレイ", + "Wind request timed out": "風のリクエストがタイムアウトしました", + "Malformed wind manifest": "風マニフェストの形式が不正です", + "Malformed wind grid": "風グリッドの形式が不正です", + "Error: {message}": "エラー:{message}", + "Needs {envVars} — add it in Provider Settings": "{envVars}が必要です。プロバイダー設定で追加してください", + "{label} is unavailable": "{label}は利用できません", + "This map stack": "このマップスタック", + "{label} unavailable: {hint}": "{label}は利用できません:{hint}", + "Google 3D tiles unavailable — check the key's API restrictions, quota, or network": "Google 3D Tilesを利用できません。キーのAPI制限、割り当て、ネットワークを確認してください", + "Needs {envVars} — add it in Provider Settings — or a Cesium ion token for the ion-hosted route": "{envVars}が必要です(プロバイダー設定で追加)。またはion経由で利用するためのCesium ionトークンが必要です", + "Exit cockpit to fly to a camera": "カメラへ移動するにはコックピットを終了してください", + "Exit cockpit to fly to a location": "場所へ移動するにはコックピットを終了してください", + "Exit cockpit to fly to a photo": "写真へ移動するにはコックピットを終了してください", + "Exit cockpit to fly to a shared view": "共有ビューへ移動するにはコックピットを終了してください", + "Exit cockpit to fly to a target": "ターゲットへ移動するにはコックピットを終了してください", + "Exit cockpit to fly to a vessel": "船舶へ移動するにはコックピットを終了してください", + "Exit cockpit to fly to a fire": "火災へ移動するにはコックピットを終了してください", + "Exit cockpit to fly to a route": "ルートへ移動するにはコックピットを終了してください", + "Exit cockpit to fly to a file": "ファイルの位置へ移動するにはコックピットを終了してください", + "Exit cockpit to fly to a scene": "シーンへ移動するにはコックピットを終了してください", + "Exit cockpit to view weather coverage": "気象のカバー範囲を表示するにはコックピットを終了してください", + "Exit cockpit to fly to a cyclone": "サイクロンへ移動するにはコックピットを終了してください", + "Exit cockpit to fly to an aircraft": "航空機へ移動するにはコックピットを終了してください", + "Exit cockpit to fly to a satellite": "衛星へ移動するにはコックピットを終了してください", + "Exit cockpit to frame contacts": "コンタクトを視野に収めるにはコックピットを終了してください", + "Visible map targets": "表示中の地図上のターゲット", + "Focusing {label}": "{label}にフォーカス中", + "Source": "ソース", + "Camera ownership changed": "カメラの制御元が変わりました", + "Saved project could not be read; storage preserved. Import a valid file to resume saving.": "保存済みのプロジェクトを読み取れませんでした。ストレージは保持されています。保存を再開するには有効なファイルをインポートしてください。", + "Ready": "準備完了", + "Scene layer turned off": "シーンのレイヤーをオフにしました", + "Stopped": "停止しました", + "Scene not saved — existing saved project could not be read. Export edits or import a valid file.": "シーンは保存されていません:既存の保存済みプロジェクトを読み取れませんでした。編集内容をエクスポートするか、有効なファイルをインポートしてください。", + "Scene not saved — {reason}": "シーンは保存されていません:{reason}", + "Scene not saved — browser storage unavailable": "シーンは保存されていません:ブラウザーのストレージを利用できません", + "Loaded: {scene} / {shot}": "読み込みました:{scene} / {shot}", + "Cannot update {scene}: shot inventory changed": "{scene}を更新できません:ショット構成が変わりました", + "Cannot update {scene}: evidence beats changed": "{scene}を更新できません:証拠の区切りが変わりました", + "Cannot update {scene}: shot bindings are incomplete": "{scene}を更新できません:ショットのバインドが不完全です", + "Updated {count} shots: {scene}": { + "other": "{count}ショットを更新しました:{scene}" + }, + "Appended {count} shots: {scene}": { + "other": "{count}ショットを追加しました:{scene}" + }, + "Cannot capture shot: camera not ready": "ショットを取得できません:カメラの準備ができていません", + "Captured: {scene} / {shot}": "取得しました:{scene} / {shot}", + "Select a shot first": "まずショットを選択してください", + "Updated: {scene} / {shot}": "更新しました:{scene} / {shot}", + "Could not leave scene: {scene}": "シーンを終了できませんでした:{scene}", + "Seeked: {scene} / {shot}": "シークしました:{scene} / {shot}", + "Seeking scene clock": "シーンクロックをシーク中", + "Camera unavailable — exit cockpit first": "カメラを使用できません:先にコックピットを終了してください", + "No shots to run": "実行するショットがありません", + "Scene transition limit reached — load a shot to reset": "シーン切り替えの上限に達しました。リセットするにはショットを読み込んでください", + "Export failed: {error}": "エクスポートに失敗しました:{error}", + "Project exported": "プロジェクトをエクスポートしました", + "Importing project": "プロジェクトをインポート中", + "Imported {file}": "{file}をインポートしました", + "Import failed: {error}": "インポートに失敗しました:{error}", + "Import failed (could not read JSON file)": "インポートに失敗しました(JSONファイルを読み取れませんでした)", + "Layers refused: {layers}": "拒否されたレイヤー:{layers}", + "Could not exit {mode} — scene layers may be refused": "{mode}を終了できませんでした。シーンのレイヤーが拒否される可能性があります", + "Camera move interrupted": "カメラの移動が中断されました", + "Scene media timed out": "シーンのメディアがタイムアウトしました", + "Action complete": "操作が完了しました", + "Action unavailable or cancelled": "操作を利用できないか、キャンセルされました", + "Scene actions": "シーンの操作", + "Select a feature or use Tab and Enter to choose an action.": "フィーチャーを選択するか、TabとEnterで操作を選んでください。", + "Selected feature: {feature}. Choose an action.": "選択したフィーチャー:{feature}。操作を選んでください。", + "Running {index}/{total}: {scene} / {shot}": "実行中 {index}/{total}:{scene} / {shot}", + "Scene run complete": "シーンの実行が完了しました", + "Bhote Koshi Evidence Sequence": "Bhote Koshi証拠シーケンス", + "Nepal Flood Incident": "ネパール洪水インシデント", + "Global Flights Radar": "グローバル航空レーダー", + "Orbital Watch": "軌道監視", + "Thermal Threat Board": "熱源脅威ボード", + "City Overload": "シティオーバーロード", + "Omniscience Pullback": "オムニサイエンス・プルバック", + "Global Incident Context": "グローバルなインシデント状況", + "Nepal-Focused Globe Rotation": "ネパールを中心とした地球の回転", + "Bhote Koshi Regional Approach": "Bhote Koshi地域への接近", + "Bhote Koshi Nearby Cities": "Bhote Koshi周辺の都市", + "Bhote Koshi Incident Corridor": "Bhote Koshiインシデント流路", + "Bhote Koshi Flood Path": "Bhote Koshi洪水の経路", + "Bhote Koshi Corridor Overview": "Bhote Koshi流路の概観", + "Bhote Koshi Upper Valley": "Bhote Koshi上流渓谷", + "Upper Valley Collapse": "上流渓谷の崩壊", + "Debris-Dammed Lake": "土砂ダム湖", + "Nepal-China Border Gate": "ネパール・中国国境ゲート", + "Timure Evidence Cluster": "Timureの証拠群", + "Syabru Besi Passage": "Syabru Besiの通過", + "Bidur / Trishuli Consequence": "Bidur / Trishuliへの影響", + "Final view": "最終ビュー", + "Scene {n}": "シーン {n}", + "Shot {n}": "ショット {n}", + "the title of scene {scene} is empty or longer than {max} characters": "シーン{scene}のタイトルが空か、{max}文字を超えています", + "the title of shot {shot} in scene {scene} is empty or longer than {max} characters": "シーン{scene}のショット{shot}のタイトルが空か、{max}文字を超えています", + "the project is larger than 5 MiB": "プロジェクトが5 MiBを超えています", + "the project has too many shots": "プロジェクトのショットが多すぎます", + "the scene data failed validation": "シーンデータの検証に失敗しました", + "Could not complete this action. Check the file, references and selected assets.": "この操作を完了できませんでした。ファイル、参照、選択したアセットを確認してください。", + "{count} scenes": { + "other": "{count}シーン" + }, + "{count} shots": { + "other": "{count}ショット" + }, + "{count} data packs": { + "other": "データパック{count}個" + }, + "{scene} / {pack}: {status}. File: {path}. {attribution} · {license}": "{scene} / {pack}:{status}。ファイル:{path}。{attribution} · {license}", + "Unavailable layers: {layers}": "利用できないレイヤー:{layers}", + "Uses registered content or linked media. Those external files are not included in a scene bundle. Their original notices still apply.": "登録済みのコンテンツまたはリンクされたメディアを使用しています。これらの外部ファイルはシーンバンドルに含まれません。元の表示事項は引き続き適用されます。", + "{bytes} bundled bytes verified. Files stay in memory for this session. Reimport the bundle after reloading the app.": "バンドルされた{bytes}バイトを検証しました。ファイルはこのセッションの間だけメモリーに保持されます。アプリを再読み込みした後は、バンドルを再インポートしてください。", + "Review scene import": "シーンのインポートを確認", + "Nothing is loaded or changed until you apply this file.": "このファイルを適用するまで、何も読み込まれず、変更もされません。", + "Ready to import": "インポートの準備ができました", + "Apply replaces the current project. Export your current project first if you want to keep both.": "適用すると現在のプロジェクトが置き換わります。両方を残したい場合は、先に現在のプロジェクトをエクスポートしてください。", + "Apply import": "インポートを適用", + "Import was not applied. The current project may have changed.": "インポートは適用されませんでした。現在のプロジェクトが変更された可能性があります。", + "Select a scene and shot first": "先にシーンとショットを選択してください", + "Edit scene details": "シーンの詳細を編集", + "Camera positions use degrees and meters above the ellipsoid. Drafts are validated before they replace your saved scene.": "カメラ位置は度と楕円体からの高さ(メートル)で指定します。下書きは検証されてから保存済みのシーンを置き換えます。", + "Anchors and data packs": "アンカーとデータパック", + "Shot camera, timing, packs and actions": "ショットのカメラ、タイミング、パック、操作", + "New anchor ID": "新しいアンカーID", + "Capture camera as anchor": "カメラ位置をアンカーとして取得", + "Anchor added to draft": "アンカーを下書きに追加しました", + "Set move start to current camera": "移動の開始点を現在のカメラに設定", + "Move added to draft; destination remains the shot camera": "移動を下書きに追加しました。移動先はショットのカメラのままです", + "Use ordinary flight": "通常の飛行を使用", + "Invalid shot JSON": "ショットのJSONが無効です", + "Apply details": "詳細を適用", + "Select a scene first": "先にシーンを選択してください", + "Share selected scene": "選択したシーンを共有", + "Scene JSON preserves authored settings and attribution. It does not include asset files. Bundles include only pack files you choose here or previously imported bundle files.": "シーンJSONには作成時の設定と出典情報が保持されます。アセットファイルは含まれません。バンドルには、ここで選択したパックファイルか、以前にインポートしたバンドルファイルのみが含まれます。", + "Download scene JSON": "シーンJSONをダウンロード", + "Choose data-pack files": "データパックのファイルを選択", + "Or choose a data-pack folder": "またはデータパックのフォルダーを選択", + "Download asset bundle": "アセットバンドルをダウンロード", + "Asset bundle downloaded": "アセットバンドルをダウンロードしました", + "No RTL-SDR device selected": "RTL-SDRデバイスが選択されていません", + "WebUSB requires desktop Chrome or Edge on localhost/HTTPS": "WebUSBには、localhostまたはHTTPS上のデスクトップ版ChromeかEdgeが必要です", + "RTL-SDR access failed; close other SDR apps and check USB permissions": "RTL-SDRにアクセスできませんでした。ほかのSDRアプリを閉じて、USBの許可を確認してください", + "RTL-SDR connection failed": "RTL-SDRの接続に失敗しました", + "Receiving 1090 MHz ADS-B": "1090 MHz ADS-Bを受信中", + "Receiving broadcast FM": "FM放送を受信中", + "Connect an RTL-SDR to begin.": "開始するにはRTL-SDRを接続してください。", + "WebUSB is unavailable in this browser": "このブラウザーではWebUSBを利用できません", + "USB work abandoned after a stalled transfer": "転送が停止したためUSB処理を中止しました", + "SDR decoder worker failed": "SDRデコーダーのワーカーが失敗しました", + "SDR decoder did not start; receiver stopped": "SDRデコーダーが起動しませんでした。受信機を停止しました", + "SDR decoder stopped responding; receiver stopped": "SDRデコーダーが応答しなくなりました。受信機を停止しました", + "SDR decoder fell behind; receiver stopped": "SDRデコーダーの処理が追いつきません。受信機を停止しました", + "WebUSB requires desktop Chrome or Edge": "WebUSBにはデスクトップ版ChromeまたはEdgeが必要です", + "Waiting for an RTL-SDR device…": "RTL-SDRデバイスの接続待ち…", + "Browser audio is suspended; press Connect again to resume it": "ブラウザーの音声が一時停止しています。再開するにはもう一度[接続]を押してください", + "RTL-SDR sample stream stopped": "RTL-SDRのサンプルストリームが停止しました", + "Switching to {mode}…": "{mode}に切り替え中…", + "Browser audio is suspended; select FM again to resume it": "ブラウザーの音声が一時停止しています。再開するにはもう一度FMを選択してください", + "RTL-SDR gain change failed": "RTL-SDRのゲイン変更に失敗しました", + "Tuned to {mhz} MHz FM": "FM {mhz} MHzに同調しました", + "Seeking up…": "上方向にシーク中…", + "Seeking down…": "下方向にシーク中…", + "Seek stopped": "シークを停止しました", + "Scanning {mhz} MHz…": "{mhz} MHzをスキャン中…", + "Found {mhz} MHz · {snr} dB": "{mhz} MHzを検出 · {snr} dB", + "FM signal found at {mhz} MHz": "{mhz} MHzでFM信号を検出しました", + "No FM signal found": "FM信号が見つかりません", + "Browser location is unavailable": "ブラウザーの位置情報を利用できません", + "Receiver location was not granted": "受信機の位置情報が許可されませんでした", + "RTL-SDR disconnected": "RTL-SDRが切断されました", + "Upstream response too large": "配信元からの応答が大きすぎます", + "Vector tile request timed out": "ベクタータイルのリクエストがタイムアウトしました", + "Vector tiles unavailable": "ベクタータイルを利用できません", + "Vector tile metadata unavailable": "ベクタータイルのメタデータを利用できません", + "Zoom in for vector tile coverage": "ズームインしてベクタータイルのカバー範囲を表示", + "Saturation": "彩度", + "Edge Thickness": "輪郭線の太さ", + "Contrast": "コントラスト", + "Grain": "粒子", + "Vignette": "周辺減光", + "Pixelation": "ピクセル化", + "Distortion": "歪み", + "Instability": "揺らぎ", + "Density": "密度", + "Gain": "ゲイン", + "Bloom": "ブルーム", + "Scanlines": "走査線", + "Sensitivity": "感度", + "WHOT/BHOT": "WHOT/BHOT", + "Ironbow": "Ironbow", + "unsaved CCTV calibration": "未保存のCCTVキャリブレーション", + "the open cockpit": "表示中のコックピット", + "Switching language reloads the page. This will be lost: {items}. Switch anyway?": "言語を切り替えるとページが再読み込みされます。次の内容は失われます:{items}。切り替えますか?", + "VISUAL PRESETS": "ビジュアルプリセット", + "LOCATION": "位置", + "CONTACTS — nearest planes · vessels · sites": "コンタクト:最寄りの航空機 · 船舶 · 施設", + "SPACE MISSIONS — launches & orbital assets": "宇宙ミッション:打ち上げと軌道上の宇宙機", + "LIVE SIGNALS": "ライブシグナル", + "Contacts could not complete the requested transition; try again": "コンタクトが要求された切り替えを完了できませんでした。もう一度お試しください", + "CCTV layer unavailable": "CCTVレイヤーを利用できません", + "Link shared": "リンクを共有しました", + "Link copied!": "リンクをコピーしました", + "Copy failed": "コピーに失敗しました", + "Return map to straight-down view": "地図を真上からの表示に戻す", + "Tilt map to oblique view": "地図を斜めの視点に傾ける", + "Reset map to north up. Current heading {heading} degrees": "地図を北が上になるようにリセット。現在の方位 {heading}度", + "Tilted view": "斜め表示", + "Straight-down view": "真上表示", + "North up": "北を上に", + "CCTV calibration saved": "CCTVキャリブレーションを保存しました", + "HDG": "HDG", + "PITCH": "ピッチ", + "FOV": "FOV", + "RANGE": "距離", + "HGT": "高さ", + "ΔN": "ΔN", + "ΔE": "ΔE", + "{value}m": "{value}m", + "CCTV calibration reset": "CCTVキャリブレーションをリセットしました", + "ADJUST ON": "調整 オン", + "ADJUST": "調整", + "SNAPSHOT": "スナップショット", + "LIVE": "ライブ", + "UPSTREAM": "配信元", + "SYNTHETIC": "合成", + "CONFIGURED": "設定済み", + "SEED": "初期データ", + "UNKNOWN": "不明", + "OK": "正常", + "ERROR": "エラー", + "Live HLS connected": "ライブHLS接続済み", + "Live HLS unavailable": "ライブHLSを利用できません", + "No stream URL configured": "ストリームURLが設定されていません", + "Live stream connected": "ライブストリーム接続済み", + "Snapshot feed connected": "スナップショットフィード接続済み", + "Media fetch failed": "メディアの取得に失敗しました", + "Upstream snapshot active": "配信元のスナップショットを使用中", + "Fallback Street View frame": "代替のストリートビュー画像", + "Upstream unavailable": "配信元を利用できません", + "No source configured": "ソースが設定されていません", + "Configured source": "設定済みのソース", + "Configured Source": "設定済みのソース", + "Synthetic fallback": "合成による代替", + "Upstream HTTP {status}": "配信元 HTTP {status}", + "Unexpected media type {type}": "予期しないメディアタイプ {type}", + "SOURCE · UNKNOWN": "ソース · 不明", + "FRAME · LOADING": "フレーム · 読込中", + "FRAME · UNAVAILABLE": "フレーム · 利用不可", + "CALIBRATED": "校正済み", + "CURATED": "精査済み", + "RAW PRIOR": "初期推定値", + "CCTV ON": "CCTV オン", + "CCTV OFF": "CCTV オフ", + "VIEWSHED ON": "可視域 オン", + "COVERAGE ON": "カバー範囲 オン", + "COVERAGE OFF": "カバー範囲 オフ", + "AUTO HOP ON": "自動切替 オン", + "AUTO HOP OFF": "自動切替 オフ", + "PROJECTION ON": "投影 オン", + "PROJECTION OFF": "投影 オフ", + "CAL · EDITED (UNSAVED)": "CAL · 編集済み(未保存)", + "CAL · {badge}": "CAL · {badge}", + "MONITOR": "モニター", + "OFF": "オフ", + "{city} · HDG {heading} · FOV {fov} · RANGE {range}m · {projection}{calBadge} · {provider}{status}": "{city} · HDG {heading} · FOV {fov} · 距離 {range}m · {projection}{calBadge} · {provider}{status}", + "{count} cameras loaded · click a camera to activate": { + "other": "カメラ {count}台を読込済み · カメラをクリックして有効化" + }, + "{count} cameras loaded · enable CCTV to activate": { + "other": "カメラ {count}台を読込済み · CCTVを有効にして使用" + }, + "{count} more cameras outside this view · pan or zoom out": { + "other": "この表示範囲外にさらに {count}台のカメラ · 移動またはズームアウト" + }, + "Enable CCTV to load camera intersections": "CCTVを有効にすると、カメラのある交差点を読み込みます", + "Enable CCTV to start camera-linked intelligence summaries.": "CCTVを有効にすると、カメラ連動のインテリジェンス概要を開始します。", + "No summary available.": "利用できる概要はありません。", + "loading frames": "フレーム読込中", + "camera grid ready": "カメラグリッド準備完了", + "Clearing selected data layers": "選択中のデータレイヤーを解除中", + "Clear selected data layers": "選択中のデータレイヤーを解除", + "CONDITIONS UNKNOWN": "気象状況不明", + "CLEAR": "快晴", + "PARTLY CLOUDY": "晴れ", + "OVERCAST": "曇天", + "FOG": "霧", + "DRIZZLE": "霧雨", + "RAIN": "雨", + "RAIN SHOWERS": "にわか雨", + "SNOW SHOWERS": "にわか雪", + "THUNDERSTORM": "雷雨", + "MIXED CONDITIONS": "その他の気象状況", + "CYCLE ON": "自動切替 オン", + "CYCLE OFF": "自動切替 オフ", + "Stop automatic page cycling. Previous, Next, and the SIG/NEWS/LOCAL tabs stay available.": "ページの自動切り替えを停止します。[前へ]、[次へ]と[シグナル]/[ニュース]/[ローカル]タブは引き続き使えます。", + "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": "地域ニュースを取得できません", + "RESOLVING REGION": "地域を特定中", + "REGION UNAVAILABLE": "地域を取得できません", + "NO RECENT LOCATION MATCHES": "この地域の最近のニュースなし", + "{domain} · {age}": "{domain} · {age}", + "{value} KM/H": "{value} km/h", + "CLOUD {pct}%": "雲量 {pct}%", + "CLOUD UNKNOWN": "雲量不明", + "{source} · LOCATION QUERY": "{source} · 位置クエリ", + "REGIONAL NEWS": "地域ニュース", + "{label} · {radius} AIR/SEA WINDOW": "{label} · 航空機/船舶の範囲 {radius}", + "{scope} · INSTALLATIONS {coverage}": "{scope} · 施設 {coverage}", + "CONTEXT STANDBY": "コンテキスト待機", + "ENABLE GLOBAL CONTEXT FOR PROXIMITY PINGS": "グローバルコンテキストを有効にすると近接ピングを表示", + "CONTACT LOST · LAST KNOWN READOUT · NOT AN ALL-CLEAR": "コンタクト喪失 · 最終既知の値 · 安全の保証ではありません", + "CONTACT LOST · {subject}": "コンタクト喪失 · {subject}", + "SUBJECT LEFT ITS FEED · READOUT HOLDING LAST KNOWN": "対象がフィードから外れました · 最終既知の値を保持", + "{cohort} · {contact}": "{cohort} · {contact}", + "NO AVAILABLE EXAMPLE": "該当する例なし", + "{cohort}, Unavailable": "{cohort}、利用不可", + "AHEAD": "正面", + "{side} {angle}": "{side} {angle}", + "L": "左", + "R": "右", + "{count} INPUTS UNKNOWN · NOT AN ALL-CLEAR": { + "other": "不明な入力 {count}件 · 安全の保証ではありません" + }, + "AVAILABLE INPUTS CURRENT · NOT AN ALL-CLEAR": "利用可能な入力は最新 · 安全の保証ではありません", + "SQUAWK {code} · {status}": "スコーク {code} · {status}", + "ADS-B PRIORITY · {status}": "ADS-B優先 · {status}", + "ADS-B EMERGENCY · {status}": "ADS-B緊急 · {status}", + "AIRCRAFT": "航空機", + "ACQUIRING SURFACE": "地表を取得中", + "SURFACE FALLBACK": "地表フォールバック", + "STALE FEED": "フィード更新遅延", + "LIVE TRACK": "ライブ追跡", + "COMMERCIAL": "民間", + "{aircraftClass} · {emergency} · {feedState}": "{aircraftClass} · {emergency} · {feedState}", + "{aircraftClass} · {feedState} · COURSE ALIGNED": "{aircraftClass} · {feedState} · 針路一致", + "ARROW · ESTIMATED DIRECTION": "矢印 · 推定方向", + "ROUTE DATA UNAVAILABLE": "経路データなし", + "DEST {bearing}": "目的地 {bearing}", + "Disable cockpit weather effects": "コックピットの天候エフェクトを無効にする", + "Enable cockpit weather effects": "コックピットの天候エフェクトを有効にする", + "ON": "オン", + "Normal": "標準", + "Night vision": "暗視", + "Thermal": "サーマル", + "Anime": "アニメ", + "Noir": "ノワール", + "Current cockpit vision style: {style}. Activate for next style.": "現在のコックピットビジョンスタイル:{style}。押すと次のスタイルに切り替わります。", + "Current style: {style} — click for next": "現在のスタイル:{style}。クリックで次へ", + "Collapse Contact panel": "コンタクトパネルを折りたたむ", + "Expand Contact panel": "コンタクトパネルを展開", + "Collapse contact panel": "コンタクトパネルを折りたたむ", + "Expand contact panel": "コンタクトパネルを展開", + "Collapse cockpit briefing panel": "コックピットブリーフィングパネルを折りたたむ", + "Expand cockpit briefing panel": "コックピットブリーフィングパネルを展開", + "Collapse briefing panel": "ブリーフィングパネルを折りたたむ", + "Expand briefing panel": "ブリーフィングパネルを展開", + "OBSERVED / MAPPED PINGS": "観測 / マップ済みピング", + "SOURCE-BACKED EVENTS · NO SYNTHETIC NEWS": "出典付きイベントのみ · 合成ニュースなし", + "LATEST LOCATION-MATCHED REPORTING": "位置に一致する最新の報道", + "{source} · LOCATION QUERY · RECENT": "{source} · 位置クエリ · 最近", + "LOCAL INFO": "ローカル情報", + "PLACE / CONDITIONS / POSITION": "場所 / 天候 / 位置", + "TIME UNKNOWN": "時刻不明", + "{count}M AGO": "{count}分前", + "{count}H AGO": "{count}時間前", + "{count}D AGO": "{count}日前", + "DIR UNKNOWN": "風向不明", + "Select flight {title}": "フライト{title}を選択", + "{aircraftClass} · CURRENT": "{aircraftClass} · 追跡中", + "MILITARY FLIGHT": "軍用機", + "COMMERCIAL FLIGHT": "民間機", + "{aircraftClass} · {distance}": "{aircraftClass} · {distance}", + "DISTANCE UNKNOWN": "距離不明", + "{count} INPUTS UNKNOWN": { + "other": "不明な入力 {count}件" + }, + "SOURCE STATUS UNAVAILABLE": "ソース状態を取得できません", + "Restore real aircraft": "実際の航空機に戻す", + "Reclassify as TR-3B": "TR-3Bに再分類", + "TRACK ACQUIRED": "追跡開始", + "{label} · COURSE {heading}°": "{label} · 針路 {heading}°", + "Context could not restore every layer; try again": "コンテキストがすべてのレイヤーを復元できませんでした。もう一度お試しください", + "No selected data layers": "選択中のデータレイヤーはありません", + "{count} data layers could not be cleared": { + "other": "データレイヤー{count}件を解除できませんでした" + }, + "Cleared {count} data layers": { + "other": "データレイヤーを{count}件解除しました" + }, + "Selected data layers could not be cleared": "選択中のデータレイヤーを解除できませんでした", + "Space Missions could not complete the requested transition; try again": "宇宙ミッションが要求された切り替えを完了できませんでした。もう一度お試しください", + "Nearby installations refreshed": "周辺施設を更新しました", + "Nearby installations could not be refreshed; try again": "周辺施設を更新できませんでした。もう一度お試しください", + "Space Missions cancellation could not restore the previous layer state": "宇宙ミッションのキャンセル時に、以前のレイヤー状態を復元できませんでした", + "That layer is unavailable in the current Context mode": "そのレイヤーは現在のコンテキストモードでは利用できません", + "{layerId} could not {action} cleanly": "{layerId}を正常に{action}できませんでした", + "start": "開始", + "stop": "停止", + "{mode} could not start because another layer did not stop cleanly": "別のレイヤーが正常に停止しなかったため、{mode}を開始できませんでした", + "Space Missions": "宇宙ミッション", + "Context": "コンテキスト", + "Rendered globe frames per second · toggle with `": "地球儀の描画フレームレート(毎秒) · `キーで切り替え", + "imported map files": "インポートした地図ファイル", + "Ground heights could not be refreshed. Try the file again when the map has loaded.": "地表高を更新できませんでした。地図の読み込み後に、もう一度このファイルをお試しください。", + "{count} map objects": { + "other": "地図オブジェクト{count}個" + }, + ", drawn with its own styles": "、独自のスタイルで描画", + "Hide {name}": "{name}を非表示", + "Show {name}": "{name}を表示", + "Fly to {name}": "{name}へ移動", + "Remove {name}": "{name}を削除", + "At most {count} files at once — remove one first.": "一度に読み込めるのは最大{count}ファイルです。先に1つ削除してください。", + "Reading {name}…": "{name}を読み取り中…", + "That file could not be read.": "ファイルを読み取れませんでした。", + "Added {count} files.": { + "other": "{count}個のファイルを追加しました。" + }, + "{count} files could not be added ({name}: {error})": { + "other": "{count}個のファイルを追加できませんでした({name}:{error})" + }, + "Some KML content was left out.": "KMLの一部の内容は読み込まれませんでした。", + "Press on the ground, not the sky": "空ではなく地表を押してください", + "Drag to size the box": "ドラッグして範囲の大きさを決めてください", + "light {source} to use your {name} key": "{source}をオンにして{name}キーを使う", + "light {source}": "{source}をオンにする", + ", or ": "か、", + "{requirement}, or {offer}": "{requirement}。または{offer}", + "Live Flights": "ライブフライト", + "Military Flights": "軍用機", + "Earthquakes (24h)": "地震(24時間)", + "Satellites": "衛星", + "Space Missions (30d)": "宇宙ミッション 30日", + "Street Traffic": "道路交通", + "CCTV": "CCTV", + "Radio": "ラジオ", + "Bikeshare": "シェアサイクル", + "Live AIS Vessels": "ライブAIS船舶", + "Mapped Installations": "マップ済み施設", + "Global Context": "グローバルコンテキスト", + "Datacenters": "データセンター", + "Dams": "ダム", + "Submarine Cables": "海底ケーブル", + "FIRMS Active Fires": "FIRMS火災検知", + "Local ADS-B": "ローカルADS-B", + "Transit": "公共交通", + "Recent Imagery": "最近の衛星画像", + "Fire Perimeters": "火災の延焼範囲", + "GNSS Integrity": "GNSSインテグリティ", + "S2 GhostMaps": "S2 GhostMaps", + "Rain radar": "降水レーダー", + "Lightning density": "雷密度", + "Cyclone advisories": "サイクロン情報", + "ALPR Cameras": "ALPRカメラ", + "Live Vessels": "ライブ船舶", + "Bike Share": "シェアサイクル", + "Cameras": "カメラ", + "Street Level": "ストリートレベル", + "Mapped ALPR Cameras": "ALPRカメラ", + "Data Centers": "データセンター", + "Active Fires": "火災検知", + "Military aircraft": "軍用機", + "Vessels": "船舶", + "CCTV + Street View fallback": "CCTV + ストリートビューで代替", + "OpenStreetMap + optional Google Maps Places": "OpenStreetMap + Google Maps Places(任意)", + "GFS / ECMWF IFS · FORECAST": "GFS / ECMWF IFS · 予報", + "NOAA nowCOAST · OBSERVED": "NOAA nowCOAST · 観測", + "NASA FIRMS · LIVE": "NASA FIRMS · ライブ", + "OSM routing": "OSMルート検索", + "data source|Local": "ローカル", + "ENABLING": "有効化中", + "DISABLING": "無効化中", + "Movement": "移動体", + "Infrastructure": "インフラ", + "Events": "イベント", + "Weather": "気象", + "Utilities": "ユーティリティ", + "Other layers": "その他のレイヤー", + "{state} · {source}": "{state} · {source}", + "UNCERTAIN · {source} · lifecycle state requires reconciliation": "不明確 · {source} · ライフサイクル状態の調整が必要です", + "{state} · {source} · {detail} · retry {seconds}s": "{state} · {source} · {detail} · {seconds}秒後に再試行", + "{state} · {source} · {detail}": "{state} · {source} · {detail}", + "never": "未取得", + "{source} · {loadingLabel}": "{source} · {loadingLabel}", + "{accepted} of {raw} records accepted": "{raw}件中{accepted}件のレコードを採用", + "incomplete snapshot": "不完全なスナップショット", + "{state} · {source} · {detail} · {ago}": "{state} · {source} · {detail} · {ago}", + "{state} · {source} · {ago} · retrying in {seconds}s": "{state} · {source} · {ago} · {seconds}秒後に再試行", + "{state} · {source} · {ago}": "{state} · {source} · {ago}", + "{source} · {ago}": "{source} · {ago}", + "UNCERTAIN": "不明確", + "status chip|UNAVAILABLE": "利用不可", + "Data source unavailable": "データソースを利用できません", + "{name}: {state}": "{name}:{state}", + "{count}s ago": "{count}秒前", + "Local FM stopped: internet radio started": "ローカルFMを停止しました:インターネットラジオを開始しました", + "IDLE": "待機", + "CONNECTING": "接続中", + "TUNING": "同調中", + "STREAMING": "受信中", + "UNSUPPORTED": "非対応", + "idle": "待機中", + "running": "実行中", + "suspended": "一時停止中", + "closed": "終了", + "feed status|live": "ライブ", + "feed status|stale": "更新遅延", + "feed status|unreachable": "接続不可", + "feed status|invalid": "無効", + "waiting for IQ": "IQデータ待ち", + "{count} messages": { + "other": "メッセージ {count}件" + }, + "DSP {count} blocks": { + "other": "DSP {count}ブロック" + }, + "DSP waiting": "DSP待機中", + "{level} dBFS audio": "音声 {level} dBFS", + "audio signal --": "音声信号 --", + "audio {state}": "音声 {state}", + "{count} HEARD": { + "other": "受信 {count}機" + }, + "DISCONNECT": "切断", + "CONNECT": "接続", + "LOCATING…": "測位中…", + "LOCATED": "測位済み", + "LOCATE": "測位", + " · read while Local ADS-B is on": " · ローカルADS-Bがオンの間に読み取り", + "Decoder feeds: {feeds}": "デコーダーフィード:{feeds}", + "Language: {language}": "言語:{language}", + "Change the interface language": "インターフェースの言語を変更", + "Interface language": "インターフェースの言語", + "Search languages": "言語を検索", + "No matching languages": "一致する言語がありません", + "📍 Location: --": "📍 位置:--", + "Landmark: --": "ランドマーク:--", + "Searched location": "検索した場所", + "ORBIT": "周回", + "Location not found": "場所が見つかりません", + "Check the spelling and press Enter to search again.": "つづりを確認し、Enterを押してもう一度検索してください。", + "Search failed": "検索に失敗しました", + "Press Enter to try again.": "Enterを押してもう一度お試しください。", + "Detailed outline unavailable": "詳細な輪郭を取得できません", + "Fly to a POI first": "先にPOIへ移動してください", + "Reset to full globe view": "地球全体ビューに戻す", + "Reset cockpit to full globe view": "コックピットを地球全体ビューに戻す", + "Resetting to full globe view": "地球全体ビューに戻しています", + "Resetting cockpit to full globe view": "コックピットを地球全体ビューに戻しています", + "MAP": "マップ", + "Layers": "レイヤー", + "Search": "検索", + "More": "その他", + "Panels": "パネル", + "panel": "パネル", + "Expand {name}": "{name}を展開", + "Collapse {name}": "{name}を折りたたむ", + "Expand Radio": "ラジオを展開", + "Collapse Radio": "ラジオを折りたたむ", + "Expand Radio section": "ラジオセクションを展開", + "Collapse Radio section": "ラジオセクションを折りたたむ", + "Panel layout updated — positions reset to new defaults": "パネルレイアウトを更新しました。位置を新しい初期値にリセットしました", + "Austin": "オースティン", + "San Francisco": "サンフランシスコ", + "New York": "ニューヨーク", + "Tokyo": "東京", + "London": "ロンドン", + "Paris": "パリ", + "Dubai": "ドバイ", + "Washington DC": "ワシントンD.C.", + "Tallinn": "タリン", + "Los Angeles": "ロサンゼルス", + "Collapse Cockpit display options": "コックピットのディスプレイ設定を折りたたむ", + "Expand Cockpit display options": "コックピットのディスプレイ設定を展開", + "Collapse Cockpit Radio controls": "コックピットのラジオ操作を折りたたむ", + "Expand Cockpit Radio controls": "コックピットのラジオ操作を展開", + "CH {index} / {total}": "CH {index} / {total}", + "NO STATIONS": "局なし", + "NO STATION AVAILABLE": "利用できる局がありません", + "{name}, station {index} of {total}": "{name}、{total}局中{index}局目", + "No station available": "利用できる局がありません", + "OFF AIR": "放送休止", + "STATION UNAVAILABLE": "局を利用できません", + "Station unavailable after directory refresh": "ディレクトリ更新後に局を利用できません", + "Radio could not {action} cleanly": "ラジオを正常に{action}できませんでした", + "SYNC": "同期", + "RECONCILE": "再調整", + "DISABLE": "無効化", + "ENABLE": "有効化", + "Reconcile Radio — lifecycle uncertain": "ラジオを再調整:ライフサイクルが不明確", + "Disable Radio": "ラジオを無効にする", + "Enable Radio": "ラジオを有効にする", + "{category} ({count})": "{category}({count})", + "DIRECTORY BAND": "ディレクトリバンド", + "{category} BAND": "{category}バンド", + "NO STATION SELECTED": "局が未選択", + "Directory metadata only": "ディレクトリのメタデータのみ", + "Loading station directory…": "局ディレクトリを読み込み中…", + "Choose a globe marker or use next.": "地球儀のマーカーを選ぶか、[次へ]を使ってください。", + "TAGS · {tags}": "タグ · {tags}", + "Play": "再生", + "PAUSE": "一時停止", + "RESUME": "再開", + "PLAY": "再生", + "selected": "選択中の", + "nearest": "最寄りの", + "{action} {target} radio station": "{target}ラジオ局を{action}", + "SYNCING": "同期中", + "READY": "準備完了", + "station": "局", + " · stale/degraded directory": " · ディレクトリ更新遅延/性能低下", + " · degraded directory": " · ディレクトリ性能低下", + " · stale directory": " · ディレクトリ更新遅延", + " · outside current filter": " · 現在の絞り込み対象外", + "Ready — playback starts only from your action": "準備完了:再生は操作したときのみ開始されます", + "Radio off": "ラジオ オフ", + "Connecting directly to broadcaster…": "放送局に直接接続中…", + "Buffering broadcaster stream…": "放送局のストリームをバッファリング中…", + "Playing {station}": "{station}を再生中", + "Paused {station}": "{station}を一時停止中", + "Broadcaster stream unavailable": "放送局のストリームを利用できません", + " · muted during voice interaction": " · 音声操作中はミュート", + " · restoring volume after voice": " · 音声操作後に音量を復元中", + " · static indicates no broadcaster audio": " · ノイズは放送局の音声がないことを示します", + " · tuning static until broadcaster starts": " · 放送が始まるまで選局ノイズを再生", + "Station unavailable after directory refresh — choose another channel": "ディレクトリ更新後に局を利用できません。別のチャンネルを選んでください", + "Radio is enabling…": "ラジオを有効化しています…", + "Radio is disabling…": "ラジオを無効化しています…", + "Radio lifecycle is uncertain — use Enable or Disable to reconcile": "ラジオのライフサイクルが不明確です。有効化または無効化で再調整してください", + "Clear reading": "読み取り値を消去", + "Earlier observation": "前の観測", + "Observed history": "観測履歴", + "Later observation": "次の観測", + "Latest": "最新", + "Newest frame per product": "プロダクトごとの最新フレーム", + "Replay observed history": "観測履歴を再生", + "Imagery on Esri · Google 3D returns when cleared": "画像はEsri上に表示 · 消去するとGoogle 3Dに戻ります", + "Point the camera at the ground to use the view": "このビューを使うにはカメラを地面に向けてください", + "That point cannot anchor a box": "その地点は範囲の基点にできません", + "Imagery stops at ±{limit}° latitude": "画像は緯度±{limit}°までです", + "{owner} is using the pointer — close it first": "{owner}がポインターを使用中です。先に閉じてください", + "Box is {size} wide · limit {limit}": "範囲の幅 {size} · 上限 {limit}", + "View is {size} wide · limit {limit}": "表示の幅 {size} · 上限 {limit}", + "VS BASEMAP": "地図と比較", + "A / B": "A / B", + "Newest low-cloud day for this box · scene cloud, not box cloud": "この範囲で雲の少ない最新の日 · シーン全体の雲量で、範囲内の雲量ではありません", + "Newest day covering this box · cloudier than 20%": "この範囲をカバーする最新の日 · 雲量20%超", + "Newest day with imagery for this box · partial coverage": "この範囲の画像がある最新の日 · 一部のみカバー", + "Newest daily overview for this box": "この範囲の最新の日次概観", + "newest clear day": "雲の少ない最新の日", + "newest day (cloudy)": "最新の日(曇り)", + "newest day (partial coverage)": "最新の日(一部カバー)", + "newest overview": "最新の概観", + "No imagery": "画像なし", + "Loading": "読み込み中", + "coverage|full": "全体", + "coverage|partial": "一部", + "unknown": "不明", + "no imagery": "画像なし", + "{percent}% cloud": "雲量 {percent}%", + "{min}–{max}% cloud": "雲量 {min}–{max}%", + "NO BOX": "範囲なし", + "SEARCHING": "検索中", + "{count} DAYS": { + "other": "{count}日分" + }, + "Zoom in or draw a smaller box": "ズームインするか、より小さい範囲を描いてください", + "Select a box or use the view": "範囲を選択するか、現在のビューを使用してください", + "Searching the last 30 days": "過去30日間を検索中", + "No days to show for this box": "この範囲で表示できる日がありません", + "Drag the divider · SWAP trades sides": "境界線をドラッグ · [入れ替え]で左右を交換", + "← → preview · A or B pins the focused day": "← → でプレビュー · AまたはBで選択中の日を固定", + "← → preview the other side · A or B pins it": "← → で反対側をプレビュー · AまたはBで固定", + "S on another day replaces it · × unpins": "別の日でSを押すと置き換え · ×で固定を解除", + "← → preview · S shows the focused day": "← → でプレビュー · Sで選択中の日を表示", + "ZOOM IN": "ズームイン", + "Fly in until the view fits the 1,000 km limit": "表示が上限の1,000 kmに収まるまで近づいてください", + "Imagery days, newest first · arrows move, S or A and B pin": "画像のある日(新しい順) · 矢印キーで移動、SまたはA・Bで固定", + "On the map": "地図上", + "Mode": "モード", + "Imagery opacity": "画像の不透明度", + "SWAP": "入れ替え", + "Trade the two sides of the divider": "境界線の左右を入れ替える", + "SELECT BOX": "範囲を選択", + "Drag a box on the map (Esc cancels)": "地図上で範囲をドラッグ(Escでキャンセル)", + "USE VIEW": "ビューを使用", + "Use the current view as the box": "現在のビューを範囲として使用", + "Forget the box and its images": "範囲とその画像を破棄", + "START HERE": "ここから開始", + "Checking": "確認中", + "PREVIEW": "プレビュー", + "Newest day with imagery for this box": "この範囲の画像がある最新の日", + "SHOW": "表示", + "Pin {day} as {slot}": "{day}を{slot}に固定", + "Show {day}": "{day}を表示", + "pinned {slot}": "{slot}に固定済み", + "shown": "表示中", + "preview": "プレビュー", + "start here": "ここから開始", + "No box": "範囲なし", + "Searching": "検索中", + "Every day is empty here": "ここではどの日も画像がありません", + "Sources off": "ソースがオフ", + "VS": "比較", + "Not set": "未設定", + "Basemap": "ベースマップ", + "Source off": "ソースがオフ", + "Unpin {slot}": "{slot}の固定を解除", + "Unpin the image": "画像の固定を解除", + "EXPORT {slot}": "{slot}を書き出し", + "EXPORT": "書き出し", + "Download {slot} as a PNG": "{slot}をPNGでダウンロード", + "Download the image as a PNG": "画像をPNGでダウンロード", + "Box {width} × {height} km": "範囲 {width} × {height} km", + "Acquired {time}": "取得時刻 {time}", + "Coverage {coverage}": "カバー範囲 {coverage}", + "START HERE · {reason}": "ここから開始 · {reason}", + "Daily overview shows little detail in a box this small": "この小さな範囲では、日次概観にほとんど詳細が写りません", + "Imagery: NASA GIBS and Worldview · HLS (Sentinel-2, Landsat 8/9) and VIIRS": "画像:NASA GIBSおよびWorldview · HLS(Sentinel-2、Landsat 8/9)とVIIRS", + "Details": "詳細", + "Times, coverage, sources": "時刻、カバー範囲、ソース", + "HIDE EMPTY DAYS": "画像のない日を隠す", + "SHOW EMPTY DAYS · {count}": "画像のない日を表示 · {count}", + "Days the probe found empty in this box": "この範囲で調査の結果、画像がなかった日", + "image": "画像", + "BASEMAP": "ベースマップ", + "The basemap": "ベースマップ", + "basemap": "ベースマップ", + "Recent imagery against the basemap divider": "最近の衛星画像とベースマップの境界線", + "Recent imagery A and B divider": "最近の衛星画像AとBの境界線", + "{before} {left} percent, {after} {right} percent": "{before} {left}%、{after} {right}%", + "Export failed · {error}": "書き出しに失敗しました · {error}", + "Scene action failed": "シーン操作に失敗しました", + "New scene name": "新しいシーン名", + "Delete scene \"{scene}\" and all shots?": "シーン「{scene}」とすべてのショットを削除しますか?", + "Delete shot \"{shot}\"?": "ショット「{shot}」を削除しますか?", + "Stopped (Esc)": "停止しました(Esc)", + "SPARSE": "低密度", + "BALANCED": "標準", + "No shots yet. Use CAPTURE SHOT to save current look.": "ショットはまだありません。[ショット取得]で現在の表示を保存してください。", + "Double-click to rename": "ダブルクリックで名前を変更", + "Shot name": "ショット名", + "LOAD": "読込", + "DEL": "削除", + "{duration}s + {hold}s": "{duration}秒 + {hold}秒", + "Choose files": "ファイルを選択", + "{count} files selected": { + "other": "{count}個のファイルを選択済み" + }, + "No file chosen": "ファイルが選択されていません", + "Cancel": "キャンセル", + "EDIT DETAILS": "詳細を編集", + "SHARE SCENE": "シーンを共有", + "Shared flight follow expired": "共有されたフライトの追従が期限切れになりました", + "Shared military flight follow expired": "共有された軍用機の追従が期限切れになりました", + "Shared satellite follow expired": "共有された衛星の追従が期限切れになりました", + "Shared flight could not be restored — feed unavailable": "共有されたフライトを復元できませんでした:フィードを利用できません", + "Shared military flight could not be restored — feed unavailable": "共有された軍用機を復元できませんでした:フィードを利用できません", + "Shared satellite could not be restored — feed unavailable": "共有された衛星を復元できませんでした:フィードを利用できません", + "Shared flight is unavailable": "共有されたフライトは利用できません", + "Shared military flight is unavailable": "共有された軍用機は利用できません", + "Shared satellite is unavailable": "共有された衛星は利用できません", + "entity": "エンティティ", + "Shared {subject} follow expired": "共有された{subject}の追従が期限切れになりました", + "Shared {subject} could not be restored — feed unavailable": "共有された{subject}を復元できませんでした:フィードを利用できません", + "Shared {subject} is unavailable": "共有された{subject}は利用できません", + "{id} isn't reporting now — showing where it was last seen": "{id}は現在報告していません。最後に確認された位置を表示しています", + "{id} isn't reporting now — showing where it was at {time}": "{id}は現在報告していません。{time}時点の位置を表示しています", + "This share link can't be opened": "この共有リンクは開けません", + "Shared vessel is unavailable": "共有された船舶は利用できません", + "ACQUIRING": "取得中", + "SHARED {subject}": "共有 {subject}", + "subject": "対象", + "LOADING LIVE DATA": "ライブデータ読込中", + "LOAD COMPLETE": "読込完了", + "LOAD CANCELLED": "読込中止", + "LOAD FAILED": "読込失敗", + "LIVE DATA OFF": "ライブデータ オフ", + "MAPPED SITES LOADED": "マップ済み施設を読込済み", + "RETRYING MAPPED SITES": "マップ済み施設を再試行中", + "FETCHING MAPPED SITES": "マップ済み施設を取得中", + "RETRYING ALPR CAMERAS": "ALPRカメラを再試行中", + "FETCHING ALPR CAMERAS": "ALPRカメラを取得中", + "OVERPASS TEMPORARILY UNAVAILABLE": "Overpassが一時的に利用不可", + "OVERPASS RATE-LIMITED": "Overpassがレート制限中", + "OVERPASS TIMED OUT": "Overpassがタイムアウト", + "OVERPASS COULD NOT COMPLETE THE QUERY": "Overpassがクエリを完了できません", + "MAP TILES TEMPORARILY UNAVAILABLE": "地図タイルが一時的に利用不可", + "MAPPED NAMES TEMPORARILY UNAVAILABLE": "施設名が一時的に利用不可", + "TURNING OFF LIVE DATA": "ライブデータをオフにしています", + "REFRESHING LIVE DATA": "ライブデータを更新中", + "syncing road network": "道路網を同期中", + "retry pending": "再試行待ち", + "retrying in {seconds}s": "{seconds}秒後に再試行", + "ALPR cameras": "ALPRカメラ", + "OpenSky OAuth client missing/invalid": "OpenSky OAuthクライアントがないか無効です", + "OpenSky OAuth rejected credentials": "OpenSky OAuthが認証情報を拒否しました", + "OpenSky username/password rejected": "OpenSkyのユーザー名/パスワードが拒否されました", + "OpenSky auth missing": "OpenSkyの認証情報がありません", + "OpenSky auth required": "OpenSkyの認証が必要です", + "OpenSky auth invalid": "OpenSkyの認証が無効です", + "OpenSky auth failed": "OpenSkyの認証に失敗しました", + "KartaView answered HTTP {status}": "KartaViewがHTTP {status}を返しました", + "Panoramax answered HTTP {status}": "PanoramaxがHTTP {status}を返しました", + "KartaView refused the request (code {code})": "KartaViewがリクエストを拒否しました(コード {code})", + "Vector tiles unavailable (HTTP {status})": "ベクタータイルを利用できません(HTTP {status})", + "GNSS feed unavailable (HTTP {status})": "GNSSフィードを利用できません(HTTP {status})", + "{count} CelesTrak groups unavailable": { + "other": "CelesTrakの{count}グループを利用できません" + }, + "{feed} feed unavailable": "{feed}フィードを利用できません", + "Source snapshot {minutes} min old": "ソースのスナップショットは{minutes}分前のものです", + "{source} rate limited (anonymous)": "{source}がレート制限中(匿名)", + "{source} rate limited": "{source}がレート制限中", + "{source} network error": "{source}のネットワークエラー", + "Malformed {source} aircraft rows": "{source}の航空機データ行の形式が不正です", + "Malformed {source} response": "{source}の応答形式が不正です", + "entry {n}": "項目 {n}", + "feeds {names} stale": { + "other": "フィード{names}が更新遅延" + }, + "feeds {names} unreachable": { + "other": "フィード{names}に接続できません" + }, + "feeds {names} invalid": { + "other": "フィード{names}が無効です" + }, + "Loading {feeds}": "{feeds}を読み込み中", + "{count} heard": { + "other": "受信 {count}機" + }, + "USB {rate} msg/s": "USB {rate} msg/s", + "{rate} msg/s": "{rate} msg/s", + "{count} feeds live": { + "other": "ライブフィード {count}件" + }, + "{feed} {count} ({seconds}s old)": "{feed} {count}({seconds}秒前)", + "Street Level toggle failed": "ストリートレベルの切り替えに失敗しました", + "Street Level could not start": "ストリートレベルを開始できませんでした", + "SHRINK": "縮小", + "EXPAND": "拡大", + "Shrink": "縮小", + "Expand": "拡大", + "ANY DATE": "全期間", + "LAST 10 YEARS": "過去10年", + "LAST 5 YEARS": "過去5年", + "LAST 3 YEARS": "過去3年", + "LAST 2 YEARS": "過去2年", + "LAST YEAR": "過去1年", + "LAST 6 MONTHS": "過去6か月", + "LAST 3 MONTHS": "過去3か月", + "LAST MONTH": "過去1か月", + "LAST {count} DAYS": { + "other": "過去{count}日" + }, + "{window} · SINCE {date}": "{window} · {date}以降", + "Turn Street Level off": "ストリートレベルをオフにする", + "Turn Street Level on": "ストリートレベルをオンにする", + "{name} imagery on": "{name}の画像 オン", + "{name} imagery off": "{name}の画像 オフ", + "Image by {creator}": "撮影者:{creator}", + " or ": "または", + "OPEN {labels}": "{labels}を開く", + "Open {names} at the centre of the view": "表示の中心で{names}を開く", + "Zoom in to a street to open {names} here": "ここで{names}を開くには通りまでズームイン", + "Switch Street Level on to draw its coverage.": "カバー範囲を描画するには、ストリートレベルをオンにしてください。", + "Every provider is switched off: light a chip to draw its coverage.": "すべてのプロバイダーがオフです。カバー範囲を描画するには、いずれかのチップをオンにしてください。", + "Loading this sequence…": "このシーケンスを読み込み中…", + "{n} images in this sequence · Esc clears": { + "other": "このシーケンスの画像 {n}枚 · Escで消去" + }, + "No sequences in view match the filter": "表示範囲内にフィルターに一致するシーケンスはありません", + "No coverage in view": "表示範囲内にカバー範囲がありません", + "{n} streets in view · click a line for its nearest photo": { + "other": "表示範囲内の通り {n}本 · 線をクリックで最寄りの写真を表示" + }, + "{n} sequences in view · click a line for its nearest photo": { + "other": "表示範囲内のシーケンス {n}件 · 線をクリックで最寄りの写真を表示" + }, + "{n} sequences in view · click a line for its photos": { + "other": "表示範囲内のシーケンス {n}件 · 線をクリックで写真を表示" + }, + "km/h": "km/h", + "m/s": "m/s", + "mph": "mph", + "hPa": "hPa", + "kt": "kt", + "kts": "kt", + "ft": "ft", + "km": "km", + "m": "m", + "h": "時間", + "min": "分", + "s": "秒", + "dB": "dB", + "Celestial ring — reveal the full globe": "天球リング:地球全体を表示", + "Celestial ring — available in Normal style": "天球リング:標準スタイルで利用可能", + "Detection overlay: {mode}": "検出オーバーレイ:{mode}", + "Detection overlay: off": "検出オーバーレイ:オフ", + "DETECT": "検出", + "{time} · {minutes} min ago": "{time} · {minutes}分前", + "Active weather": "有効な気象レイヤー", + "LATEST · newest per product": "最新 · プロダクトごとの最新", + "Forecast · valid {valid} · issued {issued}": "予報 · 対象時刻 {valid} · 発表 {issued}", + "{detail} · Does not follow history": "{detail} · 履歴とは連動しません", + "Forecast · valid {valid}": "予報 · 対象時刻 {valid}", + "voice|OFF": "オフ", + "voice|CONNECTING": "接続中", + "voice|LISTENING": "聞き取り中", + "voice|EXECUTING": "実行中", + "voice|ERROR": "エラー", + "Voice provider": "音声プロバイダー", + "Changing provider reconnects voice if it is on.": "音声がオンの場合、プロバイダーを変更すると音声が再接続されます。", + "Voice Settings": "音声設定", + "AI AGENT": "AIエージェント", + "Voice model tier — applies next session": "音声モデルのティア:次回セッションで適用", + "STD": "標準", + "Estimated session cost": "セッションの推定コスト", + "Voice control — activate to toggle voice; hold Space to speak": "音声コントロール:押すと音声のオン/オフを切り替え、Spaceを長押しで話す", + "ON/OFF": "オン/オフ", + "VOICE STANDBY": "音声待機中", + "VOICE CONTROL": "音声コントロール", + "Hold Space to speak · tap Space to activate focused controls": "Spaceを長押しで話す · Spaceをタップでフォーカス中の操作を実行", + "VOICE": "音声", + "Preview": "プレビュー", + "Settings": "設定", + "Close voice panel": "音声パネルを閉じる", + "Voice replies appear here.": "音声の応答はここに表示されます。", + "Voice assistant": "音声アシスタント", + "Dismiss voice card": "音声カードを閉じる", + "YOU": "あなた", + "THIS": "対象", + "Plan": "計画", + "Referenced items": "参照された項目", + "NOTES & SOURCES": "注記と出典", + "Show Voice Preview": "音声プレビューを表示", + "Voice inactivity timeout": "音声の無操作タイムアウト", + "Minutes": "分", + "Custom": "カスタム", + "These preferences stay in this browser.": "これらの設定はこのブラウザーにのみ保存されます。", + "VOICE SYSTEM ERROR": "音声システムエラー", + "DISMISS": "閉じる", + "Check microphone permission and network access, then try again.": "マイクの許可とネットワーク接続を確認して、もう一度お試しください。", + "Gemini returned an oversized audio chunk": "Geminiから大きすぎる音声チャンクが返されました", + "Gemini returned an unsupported audio format": "Geminiから対応していない音声形式が返されました", + "Gemini returned an invalid audio sample rate": "Geminiから無効な音声サンプルレートが返されました", + "Gemini returned incomplete PCM audio": "Geminiから不完全なPCM音声が返されました", + "This browser does not support Gemini microphone audio": "このブラウザーはGeminiのマイク音声に対応していません", + "Microphone audio processing failed": "マイク音声の処理に失敗しました", + "Gemini audio playback fell behind. Reconnect to continue.": "Geminiの音声再生が遅れています。続けるには再接続してください。", + "Gemini token response did not include an ephemeral credential": "Geminiのトークン応答に一時的な認証情報が含まれていません", + "Invalid Gemini connection endpoint": "Geminiの接続エンドポイントが無効です", + "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": "プロバイダー設定でGemini APIキーを追加してください", + "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の割り当てを利用できません。後でもう一度お試しください。", + "Gemini access was refused. Check the API key and model access.": "Geminiへのアクセスが拒否されました。APIキーとモデルへのアクセス権を確認してください。", + "Gemini Live is unavailable for the configured model.": "設定されたモデルではGemini Liveを利用できません。", + "Token request is too large": "トークンリクエストが大きすぎます", + "Use an empty JSON token request": "空のJSONトークンリクエストを使用してください", + "Release Space to send": "Spaceを離して送信", + "Hold Space to talk": "Spaceを長押しで話す", + "Voice command context is too large": "音声コマンドのコンテキストが大きすぎます", + "Gemini reliable transport exceeded its queue limit. Start voice again.": "Geminiの高信頼トランスポートがキューの上限を超えました。音声をもう一度開始してください。", + "Gemini reliable transport did not drain. Start voice again.": "Geminiの高信頼トランスポートのキューが空になりませんでした。音声をもう一度開始してください。", + "Gemini token request failed (HTTP {status})": "Geminiのトークンリクエストに失敗しました(HTTP {status})", + "Gemini token response did not include a valid model": "Geminiのトークン応答に有効なモデルが含まれていません", + "Gemini token response did not include session settings": "Geminiのトークン応答にセッション設定が含まれていません", + "Gemini session settings do not match the input mode": "Geminiのセッション設定が入力モードと一致しません", + "Requesting microphone": "マイクへのアクセスを要求中", + "Gemini could not submit this input. Start voice again.": "Geminiがこの入力を送信できませんでした。音声をもう一度開始してください。", + "Connecting to Gemini": "Geminiに接続中", + "Ask or command": "質問またはコマンドをどうぞ", + "Gemini returned an oversized message": "Geminiから大きすぎるメッセージが返されました", + "Gemini Live connection failed. Check network access, then start voice again.": "Gemini Liveの接続に失敗しました。ネットワーク接続を確認してから、音声をもう一度開始してください。", + "Gemini Live connection ended. Start voice again for a new conversation.": "Gemini Liveの接続が終了しました。新しい会話を始めるには、音声をもう一度開始してください。", + "Gemini Live could not start. Start voice again to request a fresh session.": "Gemini Liveを開始できませんでした。新しいセッションを要求するには、音声をもう一度開始してください。", + "Start voice again for a fresh token and a new conversation.": "新しいトークンで新しい会話を始めるには、音声をもう一度開始してください。", + "Start voice again to request a fresh session credential.": "新しいセッションの認証情報を要求するには、音声をもう一度開始してください。", + "Microphone permission was denied. Allow microphone access and try again.": "マイクの使用が拒否されました。マイクへのアクセスを許可して、もう一度お試しください。", + "Gemini connection timed out. Try again.": "Geminiへの接続がタイムアウトしました。もう一度お試しください。", + "Gemini voice could not start": "Gemini音声を開始できませんでした", + "Gemini did not finish the interrupted reply. Start voice again.": "Geminiが中断された応答を完了しませんでした。音声をもう一度開始してください。", + "Gemini session expiring; finishing the current reply": "Geminiのセッションがまもなく期限切れです。現在の応答を完了しています", + "Gemini rejected the voice session. Start voice again to request a fresh session.": "Geminiが音声セッションを拒否しました。新しいセッションを要求するには、音声をもう一度開始してください。", + "Start voice again for a fresh session credential. If it repeats, check Gemini model and API access.": "新しいセッションの認証情報を取得するには、音声をもう一度開始してください。繰り返し発生する場合は、GeminiのモデルとAPIへのアクセスを確認してください。", + "Gemini action history is full. Start voice again for a fresh session.": "Geminiの操作履歴がいっぱいです。新しいセッションを始めるには、音声をもう一度開始してください。", + "Gemini requested too many simultaneous actions": "Geminiが同時に要求した操作が多すぎます", + "Running command": "コマンド実行中", + "Voice text command is too long": "音声のテキストコマンドが長すぎます", + "Gemini voice is not connected": "Gemini音声が接続されていません", + "Gemini interrupted input exceeded its buffer or wait limit. Start voice again.": "Geminiの中断された入力がバッファーまたは待機の上限を超えました。音声をもう一度開始してください。", + "Gemini Live connection is expiring. Start voice again for a new conversation.": "Gemini Liveの接続がまもなく期限切れです。新しい会話を始めるには、音声をもう一度開始してください。", + "Voice off": "音声 オフ", + "Gemini voice sessions have a time limit. When a session expires, start voice again for a new conversation.": "Gemini音声のセッションには時間制限があります。セッションが期限切れになったら、音声をもう一度開始して新しい会話を始めてください。", + "Stopped by voice": "音声で停止しました", + "Automatic stop is unavailable for this voice connection.": "この音声接続では自動停止を利用できません。", + "Preview is off.": "プレビューはオフです。", + "Your choice applies now, but could not be saved.": "選択は適用されましたが、保存できませんでした。", + "WebRTC microphone support unavailable": "このブラウザーではWebRTCでマイクを使用できません", + "VOICE UNAVAILABLE": "音声利用不可", + "VOICE ACTIVE": "音声 オン", + "Voice session could not be started.": "音声セッションを開始できませんでした。", + "MINI": "ミニ", + "Next session: {pendingId} — this session stays on {modelId}": "次回セッション:{pendingId}。このセッションは{modelId}のままです", + "Voice model: {pendingId} — click to switch to {target}; applies next session": "音声モデル:{pendingId}。クリックで{target}に切り替え、次回セッションで適用", + "standard": "標準", + "mini": "ミニ", + "COST UNKNOWN": "コスト不明", + "ChatGPT OAuth session: {responses} response(s), {input} input and {output} output tokens, {captions} caption transcription(s) reported.": "ChatGPT OAuthセッション:応答{responses}件、入力トークン{input}、出力トークン{output}、字幕の文字起こし{captions}件が報告されました。", + "USD cost and the API spend cap are unavailable for this auth mode.": "この認証モードでは、米ドルでのコストとAPIの支出上限を利用できません。", + "Usage is incomplete because a response was still in flight when the session ended.": "セッション終了時に処理中の応答があったため、使用量は不完全です。", + "Estimated session cost on {modelId} — {responses} response(s). Warns at {warn}, ends the session at {cap}.": "{modelId}でのセッション推定コスト:応答{responses}件。{warn}で警告し、{cap}でセッションを終了します。", + "Estimate is incomplete — a response was still in flight when the session ended, so its usage was never reported.": "推定は不完全です。セッション終了時に処理中の応答があったため、その使用量は報告されませんでした。", + "STANDARD": "標準", + "{tier} applies next session": "{tier}は次回セッションで適用", + "Session ended — cost cap {cost}": "セッション終了:コスト上限 {cost}", + "MIC": "マイク", + "Radio did not start": "ラジオを開始できませんでした", + "{n} aircraft": { + "other": "航空機{n}機" + }, + "{n} military aircraft": { + "other": "軍用機{n}機" + }, + "{n} ships": { + "other": "船舶{n}隻" + }, + "{n} satellites": { + "other": "衛星{n}基" + }, + "{n} launches": { + "other": "打ち上げ{n}件" + }, + "{n} earthquakes": { + "other": "地震{n}件" + }, + "{n} fires": { + "other": "火災{n}件" + }, + "{n} fire perimeters": { + "other": "火災の延焼範囲{n}件" + }, + "{n} cyclones": { + "other": "サイクロン{n}個" + }, + "{n} buses": { + "other": "バス{n}台" + }, + "{n} bike share": { + "other": "シェアサイクル{n}か所" + }, + "{n} flock cameras": { + "other": "Flockカメラ{n}台" + }, + "{n} military bases": { + "other": "軍事基地{n}か所" + }, + "{n} datacenters": { + "other": "データセンター{n}か所" + }, + "{n} dams": { + "other": "ダム{n}か所" + }, + "{n} air quality": { + "other": "大気質の観測局{n}局" + }, + "{n} results": { + "other": "結果{n}件" + }, + "anywhere in the loaded data": "読み込み済みデータ全体", + "in view": "表示範囲内", + "within {radius} of the view centre": "表示中心から{radius}以内", + "within {radius} of {place} (Contacts window)": "{place}から{radius}以内(コンタクトの範囲)", + "within {radius} of {place}": "{place}から{radius}以内", + "within {radius}": "{radius}以内", + "over {region}": "{region}全域", + "At least {items}": "少なくとも{items}", + "{items} {scope}": "{scope}の{items}", + "last 24 h": "過去24時間", + "past 30 d and upcoming": "過去30日間と今後", + "the flights layer loads by viewport": "フライトレイヤーは表示範囲ごとに読み込まれます", + "distance is ground distance, not overhead": "距離は地表上の距離で、衛星までの直線距離ではありません", + "the count is a lower bound because at least one layer reached its retained limit.": "少なくとも1つのレイヤーが保持上限に達したため、この件数は下限値です。", + "{label} off": "{label} オフ", + "{label} loading": "{label}読み込み中", + "{label} error": "{label}エラー", + "counted the first {returned} loaded records": "最初に読み込まれた{returned}件を集計しました", + "counted {returned} of {total} loaded records": "読み込み済みの{total}件のうち{returned}件を集計しました", + "{layers} just turned on — still loading, counts will rise": "{layers}をオンにしたばかりです。読み込み中のため、件数は増えます", + "Contacts panel snapshot at {time} — the same retained cohort supplies the card, speech, and follow-up memory": "{time}時点のコンタクトパネルのスナップショット。カード、音声、追加の質問への記憶には同じ保持対象を使用しています", + "Contacts panel snapshot — the same retained cohort supplies the card, speech, and follow-up memory": "コンタクトパネルのスナップショット。カード、音声、追加の質問への記憶には同じ保持対象を使用しています", + "Fly below {km} km to a covered region": "{km} km未満まで高度を下げ、カバー範囲内の地域へ移動してください", + "No feed here yet · {n} regions available": { + "other": "ここにはまだフィードがありません · 利用可能な地域 {n}か所" + }, + "degraded": "性能低下", + "stale": "更新遅延", + "partial": "部分的", + "fallback": "フォールバック", + "unavailable": "利用不可", + "Not answered: {layers}": "応答なし:{layers}", + "Toggle voice control": "音声コントロールを切り替え", + "Activate to toggle voice": "押すと音声のオン/オフを切り替え", + "Voice active": "音声 オン", + "Voice could not be started.": "音声を開始できませんでした。", + "the active voice session": "進行中の音声セッション", + "{value} feet": "{value}フィート", + "Fire · FRP {frp} MW": "火災 · FRP {frp} MW", + "The flights layer is not enabled": "フライトレイヤーが有効になっていません", + "The military layer is not enabled": "軍用機レイヤーが有効になっていません", + "The satellites layer is not enabled": "衛星レイヤーが有効になっていません", + "The ais-live-vessels layer is not enabled": "ライブAIS船舶レイヤーが有効になっていません", + "Could not enable the requested layer": "要求されたレイヤーを有効にできませんでした", + "Could not disable the requested layer": "要求されたレイヤーを無効にできませんでした", + "Radio layer unavailable": "ラジオレイヤーを利用できません", + "Camera navigation is unavailable in the current view": "現在のビューではカメラのナビゲーションを利用できません", + "Camera navigation policy unavailable": "カメラのナビゲーションポリシーを利用できません", + "Nearest-aircraft selection supports Flights or Military Flights only": "最寄りの航空機の選択は、フライトまたは軍用機のみに対応しています", + "Nearest-aircraft selection needs a preset, place name, or latitude and longitude": "最寄りの航空機を選択するには、プリセット、地名、または緯度・経度が必要です", + "Could not arrive at the requested place": "要求された場所に到達できませんでした", + "The nearest airborne aircraft could not be selected": "飛行中の最寄りの航空機を選択できませんでした", + "No flights within {radius} of the current view": "現在のビューから{radius}以内にフライトはありません", + "No military within {radius} of the current view": "現在のビューから{radius}以内に軍用機はありません", + "No satellites within {radius} of the current view": "現在のビューから{radius}以内に衛星はありません", + "No vessels within {radius} of the current view": "現在のビューから{radius}以内に船舶はありません", + "{layer} is enabled on the {source} feed, but no airborne aircraft is loaded in the {place} view yet": "{layer}は{source}フィードで有効ですが、{place}のビューに飛行中の航空機はまだ読み込まれていません", + "{layer} is enabled, but no airborne aircraft is loaded in the {place} view yet": "{layer}は有効ですが、{place}のビューに飛行中の航空機はまだ読み込まれていません", + "{layer} is enabled, but its aircraft feed is unavailable": "{layer}は有効ですが、航空機フィードを利用できません", + "{layer} is enabled, but its destination refresh did not complete": "{layer}は有効ですが、目的地の更新が完了しませんでした", + "{layer} is enabled, but {source} is unavailable": "{layer}は有効ですが、{source}を利用できません", + "Could not arrive at {place}": "{place}に到達できませんでした", + "{name} is no longer loaded": "{name}は読み込まれていません", + "Unknown target layer: {target}": "不明なターゲットレイヤー:{target}", + "nominal": "標準", + "{label} on": "{label} オン", + "Frame aircraft": "航空機を視野に収める", + "Frame military aircraft": "軍用機を視野に収める", + "Frame ships": "船舶を視野に収める", + "Frame satellites": "衛星を視野に収める", + "Frame contacts": "コンタクトを視野に収める", + "{count} aircraft in frame": { + "other": "視野内の航空機 {count}機" + }, + "{count} military aircraft in frame": { + "other": "視野内の軍用機 {count}機" + }, + "{count} ships in frame": { + "other": "視野内の船舶 {count}隻" + }, + "{count} satellites in frame": { + "other": "視野内の衛星 {count}基" + }, + "{count} contacts in frame": { + "other": "視野内のコンタクト {count}件" + }, + "Nearest military aircraft": "最寄りの軍用機", + "Nearest ships": "最寄りの船舶", + "Nearest satellites": "最寄りの衛星", + "Nearest aircraft": "最寄りの航空機", + "{first} and {second}": "{first}と{second}", + "Layer": "レイヤー", + "there": "そこ", + "Camera range {range}": "カメラ距離 {range}", + "Within {radius} of the view": "ビューから{radius}以内", + "Labels on": "ラベル オン", + "Framing uses the nearest 80 loaded contacts": "視野に収める対象は、読み込み済みの最寄りの80件のコンタクトです", + "{distance} from {location}": "{location}から{distance}", + "Altitude {altitude}": "高度 {altitude}", + "{origin} to {destination}": "{origin}発{destination}行き", + "from {origin}": "{origin}発", + "to {destination}": "{destination}行き", + "Registration {registration}": "登録記号 {registration}", + "View state": "ビューの状態", + "Marked {count} places": { + "other": "{count}か所をマークしました" + }, + "Nothing marked": "マークしたものはありません", + "Not found: {places}": "見つかりません:{places}", + "Tracing outlines": "輪郭を描画中", + "Frame flights": "フライトを視野に収める", + "Frame military": "軍用機を視野に収める", + "Frame vessels": "船舶を視野に収める", + "Frame {target}": "{target}を視野に収める", + "Zoom out to globe": "地球全体までズームアウト", + "Open data layers": "データレイヤーを開く", + "Open or close a panel": "パネルを開閉", + "Change context mode": "コンテキストモードを変更", + "Control cockpit": "コックピットを操作", + "Change visual style": "ビジュアルスタイルを変更", + "Adjust HUD": "HUDを調整", + "Adjust cyber sonar": "サイバーソナーを調整", + "Adjust detection overlay": "検出オーバーレイを調整", + "Change map stack": "マップスタックを変更", + "Adjust post-processing": "ポストプロセスを調整", + "Control scene": "シーンを操作", + "Control CCTV": "CCTVを操作", + "Control radio": "ラジオを操作", + "Track contact": "コンタクトを追跡", + "Stop tracking": "追跡を停止", + "Clear marks": "マークを消去", + "Move camera": "カメラを移動", + "Fly route": "ルートを飛行", + "Analyst query": "分析クエリ", + "Next ISS pass": "次のISS通過", + "Next satellite pass": "次の衛星通過", + "Mark the map": "地図にマーク", + "Mark {targets} +{more}": "{targets}をマーク +{more}", + "Mark {targets}": "{targets}をマーク", + "Fly to {place}": "{place}へ移動", + "Fly to Location": "場所へ移動", + "Nearest aircraft to {place}": "{place}の最寄りの航空機", + "Nearest aircraft to Here": "ここの最寄りの航空機", + "Layer on": "レイヤー オン", + "Layer off": "レイヤー オフ", + "Zoom out": "ズームアウト", + "Zoom in": "ズームイン", + "Read the view": "ビューの内容を読み取り", + "Check view state": "ビューの状態を確認", + "Action": "操作", + "Finding places": "場所を検索中", + "Tracing outline": "輪郭を描画中", + "Looking up place": "場所を照会中", + "Flying": "移動中", + "Turning on layer": "レイヤーを有効化中", + "Loading aircraft": "航空機を読み込み中", + "Picking nearest": "最寄りを選択中", + "Working": "処理中", + "in progress": "処理中", + "done": "完了", + "failed": "失敗", + "cancelled": "キャンセル済み", + "Outline traced": "輪郭を描画しました", + "Outline unavailable": "輪郭を取得できません", + "Source: {label}": "出典:{label}", + "WORKING": "処理中", + "DONE": "完了", + "Recent speech": "最近の発話", + "HERE": "ここ", + "Aircraft cockpit view": "航空機コックピットビュー", + "LEVEL": "水平", + "Estimated destination direction": "推定目的地の方向", + "DEST ---°": "目的地 ---°", + "OPTICAL PLANE · 01": "光学面 · 01", + "VISOR LOCK · ACTIVE": "バイザーロック · 有効", + "GROUND SPEED · KTS": "対地速度 · KT", + "ALTITUDE · FT": "高度 · FT", + "FIRST PERSON": "一人称視点", + "LIVE TRACK · COURSE ALIGNED": "ライブ追跡 · 針路一致", + "Cockpit vision style": "コックピットのビジョンスタイル", + "Previous cockpit vision style": "前のコックピットビジョンスタイル", + "Previous vision style": "前のビジョンスタイル", + "Current cockpit vision style: NORMAL. Activate for next style.": "現在のコックピットビジョンスタイル:標準。押すと次のスタイルに切り替わります。", + "Current style: NORMAL — click for next": "現在のスタイル:標準。クリックで次へ", + "CURRENT": "現在", + "Next cockpit vision style": "次のコックピットビジョンスタイル", + "Next vision style": "次のビジョンスタイル", + "Current aircraft heading": "現在の機首方位", + "GROUND SPEED": "対地速度", + "KTS": "KT", + "ALTITUDE": "高度", + "FT": "FT", + "Contact cockpit summary": "コンタクトのコックピット概要", + "CONTACT": "コンタクト", + "CONTACTS · 250 KM": "コンタクト · 250 km", + "Contact navigation": "コンタクトナビゲーション", + "CONTEXT ONLY": "参考情報のみ", + "Nearby cohort counts": "周辺の種別ごとの件数", + "FLT": "FLT", + "MIL": "軍用", + "SITE": "施設", + "NEAREST OBSERVED / MAPPED": "最寄りの観測 / マップ済み", + "AVAILABLE INPUTS ONLY · NOT AN ALL-CLEAR": "利用可能な入力のみ · 安全の保証ではありません", + "WX": "気象", + "Cockpit briefing carousel": "コックピットブリーフィングカルーセル", + "Estimated flight plan": "推定フライトプラン", + "ESTIMATED FLIGHT PLAN": "推定フライトプラン", + "FROM": "出発", + "TO": "到着", + "Cockpit briefing controls": "コックピットブリーフィングの操作", + "Previous briefing page": "前のブリーフィングページ", + "Next briefing page": "次のブリーフィングページ", + "Live signals": "ライブシグナル", + "Latest regional news": "最新の地域ニュース", + "Location-based information": "位置に基づく情報", + "TEMP": "気温", + "WIND": "風", + "SKY": "天気", + "PRECIP": "降水", + "MM": "mm", + "Weather data by Open-Meteo.com": "気象データ提供:Open-Meteo.com", + "Cockpit briefing pages": "コックピットのブリーフィングページ", + "SIG": "シグナル", + "Show Live Signals": "ライブシグナルを表示", + "NEWS": "ニュース", + "Show Regional News": "地域ニュースを表示", + "LOCAL": "ローカル", + "Show Local Info": "ローカル情報を表示", + "Cockpit display and Radio controls": "コックピットのディスプレイとラジオの操作", + "DISPLAY": "ディスプレイ", + "Cockpit display options": "コックピットのディスプレイ設定", + "RADIO": "ラジオ", + "Cockpit compact Radio controls": "コックピットのコンパクトラジオ操作", + "Previous filtered radio station": "絞り込み中の前のラジオ局", + "Play selected radio station": "選択したラジオ局を再生", + "Next filtered radio station": "絞り込み中の次のラジオ局", + "VOLUME": "音量", + "Cockpit Radio volume": "コックピットラジオの音量", + "ESC EXIT": "Escで終了", + "C TOGGLE": "Cで切替", + "View switcher": "ビュー切替", + "Exit cockpit and return to full globe view": "コックピットを終了して地球全体ビューに戻る", + "RESET": "リセット", + "Exit cockpit view": "コックピットビューを終了", + "EXIT COCKPIT": "コックピット終了", + "Navigation, voice, and visual preset controls": "ナビゲーション、音声、ビジュアルプリセットの操作", + "Expand Visual Presets": "ビジュアルプリセットを展開", + "Pin visual presets": "ビジュアルプリセットを固定", + "Keep visual presets open": "ビジュアルプリセットを開いたままにする", + "Show the globe without a visual filter.": "ビジュアルフィルターなしで地球儀を表示します。", + "Emulate a green phosphor CRT with scanlines and screen curvature.": "走査線と画面の湾曲を備えた、緑色蛍光体のCRTを再現します。", + "Simulate night-vision goggles with green intensification and a tube vignette.": "緑色の光増幅とチューブ周辺の減光で、暗視ゴーグルを再現します。", + "Simulate FLIR-style thermal contrast. Turn up Ironbow for color.": "FLIR風の熱コントラストを再現します。色を付けるにはIronbowを上げてください。", + "Apply bright cel-shaded color and illustrated outlines.": "明るいセルシェーディングの色と輪郭線を適用します。", + "Apply high-contrast monochrome film-noir grading.": "コントラストの強いモノクロのフィルムノワール調に仕上げます。", + "MAP SOURCE": "マップソース", + "Map source": "マップソース", + "Style": "スタイル", + "Expand LOCATION": "位置を展開", + "Collapse panel": "パネルを折りたたむ", + "Pin location tray": "位置トレイを固定", + "Keep location tray open": "位置トレイを開いたままにする", + "Search any location": "場所を検索", + "Search location by name or coordinates": "名前または座標で場所を検索", + "Search any location...": "場所を検索…", + "WEATHER": "気象", + "Active weather products": "有効な気象プロダクト", + "RECENT IMAGERY": "最近の衛星画像", + "Imagery days in the box": "範囲内の画像のある日", + "CONTEXT": "コンテキスト", + "Expand panel": "パネルを展開", + "Context mode": "コンテキストモード", + "CONTACTS": "コンタクト", + "Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "選択した種類(航空機、船舶、施設)の最寄りのコンタクトを順に切り替えます。衛星は独立して追跡されます。", + "SPACE MISSIONS": "宇宙ミッション", + "SELECT CONTEXT": "コンテキストを選択", + "Contact Context actions": "コンタクトコンテキストの操作", + "COCKPIT": "コックピット", + "SEARCH NEARBY SITES": "周辺施設を検索", + "Reclassify tracked contact as TR-3B": "追跡中のコンタクトをTR-3Bに再分類", + "CONTACTS CONTEXT OFF": "コンタクトコンテキスト オフ", + "SELECT CONTACTS TO LOAD OBSERVED / MAPPED PROXIMITY": "コンタクトを選択して、観測 / マップ済みの近接情報を読み込み", + "Available Space Missions": "利用可能な宇宙ミッション", + "AVAILABLE MISSIONS": "利用可能なミッション", + "SELECT A MISSION TO INSPECT": "ミッションを選択して詳細を表示", + "LOADING 30-DAY MISSION INDEX": "30日間のミッション一覧を読込中", + "TAB PREVIEWS · ENTER / SPACE SELECTS": "Tabでプレビュー · Enter / Spaceで選択", + "Internet radio and local SDR": "インターネットラジオとローカルSDR", + "STATION TAG": "局タグ", + "Filter stations by station tag": "局タグで局を絞り込み", + "All": "すべて", + "Enable Radio, then choose a globe marker or use next.": "ラジオを有効にしてから、地球儀のマーカーを選ぶか、[次へ]を使ってください。", + "DRAG TO TUNE": "ドラッグで選局", + "Tune available internet radio stations": "利用可能なインターネットラジオ局を選局", + "ALL · DRAG THE NEEDLE": "すべて · 針をドラッグ", + "SNAPS TO AVAILABLE STATIONS": "利用可能な局にスナップ", + "Radio playback": "ラジオ再生", + "Previous filtered station": "絞り込み中の前の局", + "Play selected station": "選択した局を再生", + "Next filtered station": "絞り込み中の次の局", + "STOP": "停止", + "Stop radio playback": "ラジオ再生を停止", + "Radio volume": "ラジオの音量", + "STATION SITE": "放送局サイト", + "DIRECTORY: RADIO BROWSER": "ディレクトリ:Radio Browser", + "Audio connects directly to the broadcaster after you press 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台のローカルチューナーを共有します。", + "Use your location for faster ADS-B position decoding": "位置情報を使ってADS-Bの位置デコードを高速化", + "CHANGE DEVICE": "デバイスを変更", + "Choose a different RTL-SDR or receiver channel": "別のRTL-SDRまたは受信チャンネルを選択", + "RTL-SDR receiver mode": "RTL-SDRの受信モード", + "GAIN": "ゲイン", + "RTL-SDR tuner gain for the current mode": "現在のモードのRTL-SDRチューナーゲイン", + "AUTO": "自動", + "MSG/S": "MSG/S", + "HEARD": "受信", + "POSITIONED": "位置特定済み", + "FM MHZ": "FM MHZ", + "TUNE": "同調", + "Seek broadcast FM stations": "FM放送局をシーク", + "◀ SEEK": "◀ シーク", + "SEEK ▶": "シーク ▶", + "SDR VOL": "SDR音量", + "Local SDR volume": "ローカルSDRの音量", + "Intelligence HUD (H)": "インテリジェンスHUD(H)", + "Layout": "レイアウト", + "HUD layout": "HUDレイアウト", + "Tactical": "タクティカル", + "Minimal": "ミニマル", + "Cyber": "サイバー", + "Sonar": "ソナー", + "Cyber sonar contact scanner": "サイバーソナーのコンタクトスキャナー", + "Rings": "リング数", + "Sonar ring count": "ソナーのリング数", + "Range": "範囲", + "Sonar ring range": "ソナーのリング範囲", + "Power": "強度", + "Sonar intensity": "ソナーの強度", + "Opacity": "不透明度", + "Cyber contact and label opacity": "サイバーのコンタクトとラベルの不透明度", + "Minimum opacity for map dots, icons, models, brackets, and labels between sonar passes": "ソナーの走査の合間における地図上の点、アイコン、モデル、ブラケット、ラベルの最小不透明度", + "Sector": "扇形", + "Sonar sector width": "ソナーの扇形の幅", + "Detection Overlay (D)": "検出オーバーレイ(D)", + "Detection overlay": "検出オーバーレイ", + "Detection label density": "検出ラベルの密度", + "Allocation": "配分", + "Detection label allocation": "検出ラベルの配分", + "Elastic": "エラスティック", + "Weighted": "加重", + "Fade": "フェード", + "Detection fade distance": "検出のフェード距離", + "World-overlay fade distance outside the keyhole as a percentage of its radius": "キーホール外のワールドオーバーレイのフェード距離(キーホール半径に対する割合)", + "Outside": "外側", + "Detection opacity outside the keyhole": "キーホール外の検出の不透明度", + "World-overlay label and card opacity beyond the fade distance": "フェード距離を超えた範囲のワールドオーバーレイのラベルとカードの不透明度", + "PARAMETERS": "パラメーター", + "3D aircraft — flat icons zoomed out, 3D models up close": "3D航空機:遠景はフラットアイコン、近景は3Dモデル", + "Models": "モデル", + "3D model coverage": "3Dモデルの適用範囲", + "Proximity": "近接", + "Scope — the circular viewport mask": "スコープ:円形のビューポートマスク", + "Scope": "スコープ", + "Feather": "ぼかし", + "Scope edge feather": "スコープ縁のぼかし", + "Scope edge feather as a percentage of the keyhole radius": "スコープ縁のぼかし(キーホール半径に対する割合)", + "Draw on the world — click vertices, double-click or Enter to finish, Esc to cancel": "地球上に描画:クリックで頂点を追加、ダブルクリックまたはEnterで完了、Escでキャンセル", + "Shape": "図形", + "Shape to draw": "描画する図形", + "Area": "範囲", + "Line": "線", + "Pin": "ピン", + "Label (optional)": "ラベル(任意)", + "Label for the drawn shape": "描画した図形のラベル", + "Colour of the drawn shape": "描画した図形の色", + "Primary": "プライマリー", + "Amber": "アンバー", + "Cyan": "シアン", + "Green": "緑", + "Red": "赤", + "Clear": "消去", + "Remove every mark from the board": "ボードからすべてのマークを削除", + "Show your own GeoJSON, KML, KMZ or GPX files on the globe (or drop them onto it). File bytes stay on this device; overlays can appear in map screenshots shared with voice.": "手元のGeoJSON、KML、KMZ、GPXファイルを地球上に表示します(地球にドロップすることもできます)。ファイルのデータはこのデバイスから出ません。オーバーレイは、音声アシスタントと共有される地図のスクリーンショットに写る場合があります。", + "Import": "インポート", + "Choose GeoJSON, KML, KMZ or GPX files": "GeoJSON、KML、KMZ、GPXファイルを選択", + "Imported files": "インポートしたファイル", + "Celestial": "天球", + "Hide UI chrome": "UIを非表示", + "Clean UI": "クリーンUI", + "Bloom / Glow": "ブルーム / グロー", + "Bloom intensity": "ブルームの強度", + "Sharpening": "シャープ化", + "Sharpen": "シャープ", + "Sharpen intensity": "シャープの強度", + "EXIT CLEAN VIEW": "クリーンビュー終了", + "Return UI controls": "UIコントロールを戻す", + "Initializing photorealistic world...": "フォトリアルな世界を初期化中…", + "DATA LAYERS": "データレイヤー", + "ACQUIRING FRAME": "フレームを取得中", + "NEAREST": "最寄り", + "CCTV camera": "CCTVカメラ", + "CALIBRATION": "キャリブレーション", + "Drag the camera in the world: rings rotate, arrows move, handles set range/FOV": "ワールド上でカメラをドラッグ:リングで回転、矢印で移動、ハンドルで距離/FOVを設定", + "Camera pose — click a value to type": "カメラの姿勢:値をクリックして入力", + "Heading (compass °) — click to type": "方位(コンパス °):クリックして入力", + "Pitch (° up/down) — click to type": "ピッチ(上下 °):クリックして入力", + "Horizontal FOV (°) — click to type": "水平FOV(°):クリックして入力", + "Range / monitor-plane distance (m) — click to type": "距離 / モニター面までの距離(m):クリックして入力", + "Mount height above ground (m) — click to type": "地上からの設置高(m):クリックして入力", + "North offset from catalog position (m) — click to type": "カタログ位置からの北方向オフセット(m):クリックして入力", + "East offset from catalog position (m) — click to type": "カタログ位置からの東方向オフセット(m):クリックして入力", + "SAVE CAL": "CAL保存", + "RESET CAL": "CALリセット", + "SCENE SUMMARY": "シーン概要", + "STREET LEVEL": "ストリートレベル", + "Turn Street Level on or off": "ストリートレベルのオン/オフ", + "Show the street-level view full screen (Esc shrinks it)": "ストリートレベルのビューを全画面表示(Escで縮小)", + "Image fit": "画像の表示方法", + "FIT": "全体", + "Fit: show the whole photo, with dark bars where its shape differs from the frame": "全体:写真全体を表示し、枠と縦横比が異なる部分は黒帯で埋めます", + "FILL": "全面", + "Fill: fill the frame edge to edge, cropping the photo": "全面:枠いっぱいに表示し、写真をトリミングします", + "Close the image and deselect it on the map": "画像を閉じて地図上の選択を解除", + "Close the street-level image": "ストリートレベル画像を閉じる", + "Street-level viewer": "ストリートレベルビューアー", + "Open this image on the provider's site": "この画像をプロバイダーのサイトで開く", + "Imagery providers": "画像プロバイダー", + "PROVIDERS": "プロバイダー", + "Imagery filters": "画像フィルター", + "IMAGERY": "画像", + "Panorama filter": "パノラマフィルター", + "ALL": "すべて", + "FLAT": "平面", + "SINCE": "期間", + "SCENES": "シーン", + "Scene recipe": "シーンレシピ", + "NEW": "新規", + "CAPTURE SHOT": "ショット取得", + "UPDATE SHOT": "ショット更新", + "START": "開始", + "EXPORT PRESETS": "エクスポート", + "IMPORT": "インポート", + "RUN LOG": "実行ログ", + "POWER UP": "パワーアップ", + "GROUND STATION · PROVIDER SETTINGS": "地上局 · プロバイダー設定", + "Close key setup": "キー設定を閉じる", + "Power up the globe": "地球儀をパワーアップ", + "The globe already flies keyless. Every key below switches on another real feed — paste one and it's saved into this app's local configuration, then the server restarts itself. Server-side keys stay on this machine; Google Maps and Cesium ion run in the browser and must be provider-restricted. Keys you configured elsewhere are shown but never touched.": "地球儀はキーなしでもそのまま使えます。下の各キーは、それぞれ別の実データフィードを有効にします。キーを貼り付けるとこのアプリのローカル設定に保存され、サーバーが自動で再起動します。サーバー側のキーはこのマシンから出ません。Google MapsとCesium ionはブラウザーで動作するため、プロバイダー側で利用制限を設定してください。他の場所で設定済みのキーは表示のみで、変更されません。", + "SAVE KEYS": "キーを保存", + "ESC to close": "Escで閉じる", + "The Google Maps key buys the photorealistic planet — everything else stacks on top.": "Google Mapsのキーでフォトリアルな地球が使えるようになり、他のすべてはその上に重なります。", + "NO PLACE LEFT BEHIND": "取り残される場所はない", + "ACTIVE STYLE": "現在のスタイル", + "Globe actions": "地球儀の操作", + "Turn off all selected data layers": "選択中のデータレイヤーをすべてオフにする", + "Copy share link": "共有リンクをコピー", + "Toggle straight-down and tilted map views": "真上表示と斜め表示を切り替え", + "Reset map to north up": "地図を北が上になるようにリセット", + "Reset map bearing to north": "地図の方位を北にリセット", + "Reset camera and return to full globe view": "カメラをリセットして地球全体ビューに戻す", + "MISSION CONTROL · FIRST LAUNCH": "ミッションコントロール · 初回起動", + "Choose your first view": "最初のビューを選択", + "LIVE CONTACTS": "ライブコンタクト", + "Aircraft, vessels and nearby intelligence": "航空機、船舶、周辺のインテリジェンス", + "Launches, spacecraft and orbital context": "打ち上げ、宇宙機、軌道の状況", + "EXPLORE MANUALLY": "手動で探索", + "Begin with a clean globe": "まっさらな地球儀から始める", + "Don't show this again": "今後表示しない", + "ESC to dismiss": "Escで閉じる", + "Tip: the GEV MIC button in the dock lets you talk to the map.": "ヒント:ドックの[マイク]ボタンで地図に話しかけられます。", + "routing busy — too many routes at once": "ルート検索サービスが混雑しています。同時に要求されたルートが多すぎます", + "route proxy error": "ルート検索プロキシでエラーが発生しました", + "Malformed cyclone snapshot": "サイクロンのスナップショットの形式が不正です", + "Cyclone request timed out": "サイクロンのリクエストがタイムアウトしました" +} diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 100e046b9..7a85edcd6 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -1625,6 +1625,11 @@ "many": "{count} de câmeras carregadas · ative o CCTV para usar", "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 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.", "No summary available.": "Nenhum resumo disponível.", @@ -1957,10 +1962,6 @@ "STATION UNAVAILABLE": "ESTAÇÃO INDISPONÍVEL", "Station unavailable after directory refresh": "Estação indisponível após atualizar o diretório", "Radio could not {action} cleanly": "Não foi possível {action} o rádio corretamente", - "Go to expanded Radio section": "Ir para a seção de rádio expandida", - "Expand Radio section in Context": "Expandir a seção de rádio em \"Contexto\"", - "Close compact Radio controls": "Fechar controles compactos do rádio", - "Open compact Radio controls": "Abrir controles compactos do rádio", "SYNC": "SINC", "RECONCILE": "RECONCILIAR", "DISABLE": "DESATIVAR", @@ -1983,9 +1984,6 @@ "selected": "selecionada", "nearest": "mais próxima", "{action} {target} radio station": "{action} estação de rádio {target}", - "RADIO STATE UNCERTAIN": "ESTADO DO RÁDIO INCERTO", - "SYNCING DIRECTORY": "SINCRONIZANDO DIRETÓRIO", - "RADIO READY": "RÁDIO PRONTO", "SYNCING": "SINCRONIZANDO", "READY": "PRONTO", "station": "estação", @@ -2797,11 +2795,6 @@ "RECENT IMAGERY": "IMAGENS RECENTES", "Imagery days in the box": "Dias com imagens na área", "CONTEXT": "CONTEXTO", - "Compact Radio controls": "Controles compactos do rádio", - "Open detailed Radio controls": "Abrir controles detalhados do rádio", - "Previous station": "Estação anterior", - "Next station": "Próxima estação", - "Compact Radio volume": "Volume do rádio compacto", "Expand panel": "Expandir painel", "Context mode": "Modo de contexto", "CONTACTS": "CONTATOS", @@ -2862,7 +2855,6 @@ "Layout": "Layout", "HUD layout": "Layout do HUD", "Tactical": "Tático", - "Operator": "Operador", "Minimal": "Mínimo", "Cyber": "Ciber", "Sonar": "Sonar", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index fba4d3309..3e0932b11 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -342,6 +342,12 @@ "many": "Загружено {count} камер · включите CCTV, чтобы активировать", "other": "Загружено {count} камеры · включите CCTV, чтобы активировать" }, + "{count} more cameras outside this view · pan or zoom out": { + "one": "Ещё {count} камера вне этого вида · сдвиньте или отдалите", + "few": "Ещё {count} камеры вне этого вида · сдвиньте или отдалите", + "many": "Ещё {count} камер вне этого вида · сдвиньте или отдалите", + "other": "Ещё {count} камеры вне этого вида · сдвиньте или отдалите" + }, "Enable CCTV to load camera intersections": "Включите CCTV, чтобы загрузить камеры на перекрёстках", "Enable CCTV to start camera-linked intelligence summaries.": "Включите CCTV, чтобы запускать сводки по камерам.", "No summary available.": "Сводка недоступна.", @@ -552,8 +558,6 @@ "Expand Cockpit display options": "Развернуть параметры экрана кабины", "Collapse Cockpit Radio controls": "Свернуть управление радио кабины", "Expand Cockpit Radio controls": "Развернуть управление радио кабины", - "Close compact Radio controls": "Закрыть компактное управление радио", - "Open compact Radio controls": "Открыть компактное управление радио", "DIRECTORY BAND": "ДИАПАЗОН КАТАЛОГА", "{category} BAND": "ДИАПАЗОН {category}", "NO STATION SELECTED": "СТАНЦИЯ НЕ ВЫБРАНА", @@ -567,9 +571,6 @@ "selected": "выбранную", "nearest": "ближайшую", "{action} {target} radio station": "{action} {target} радиостанцию", - "RADIO STATE UNCERTAIN": "СОСТОЯНИЕ РАДИО НЕОПРЕДЕЛЁННО", - "SYNCING DIRECTORY": "СИНХРОНИЗАЦИЯ КАТАЛОГА", - "RADIO READY": "РАДИО ГОТОВО", "SYNCING": "СИНХРОНИЗАЦИЯ", "READY": "ГОТОВО", "station": "станция", @@ -708,9 +709,6 @@ "FLIR": "FLIR", "Style": "Стиль", "STATION SITE": "САЙТ СТАНЦИИ", - "Open detailed Radio controls": "Открыть подробное управление радио", - "Previous station": "Предыдущая станция", - "Next station": "Следующая станция", "Expand panel": "Развернуть панель", "CONTACTS": "КОНТАКТЫ", "Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently.": "Перебирает ближайшие контакты выбранного типа — самолёты, суда, объекты. Спутники отслеживаются отдельно.", @@ -722,13 +720,11 @@ "Next filtered station": "Следующая отфильтрованная станция", "STOP": "СТОП", "Stop radio playback": "Остановить воспроизведение радио", - "Compact Radio controls": "Компактное управление радио", "Context mode": "Режим контекста", "Contact Context actions": "Действия контекста контакта", "Available Space Missions": "Доступные космические миссии", "LOADING 30-DAY MISSION INDEX": "ЗАГРУЗКА ИНДЕКСА МИССИЙ ЗА 30 ДНЕЙ", "Radio playback": "Воспроизведение радио", - "Compact Radio volume": "Громкость компактного радио", "Radio volume": "Громкость радио", "STATION TAG": "ТЕГ СТАНЦИИ", "DRAG TO TUNE": "ТЯНИТЕ ДЛЯ НАСТРОЙКИ", @@ -775,7 +771,6 @@ "Fade": "Затухание", "Outside": "Снаружи", "Tactical": "Тактический", - "Operator": "Операторский", "Minimal": "Минимальный", "HUD layout": "Макет HUD", "Layout": "Макет", @@ -2483,8 +2478,6 @@ "Exit cockpit to fly to a route": "Выйдите из кабины, чтобы перелететь к маршруту", "Exit cockpit to fly to a file": "Выйдите из кабины, чтобы перелететь к файлу", "Exit cockpit to fly to a scene": "Выйдите из кабины, чтобы перелететь к сцене", - "Go to expanded Radio section": "Перейти к развёрнутому разделу радио", - "Expand Radio section in Context": "Развернуть раздел «Радио» в панели «Контекст»", "New scene name": "Название новой сцены", "Delete scene \"{scene}\" and all shots?": "Удалить сцену «{scene}» и все кадры?", "Detailed outline unavailable": "Подробный контур недоступен", diff --git a/src/i18n/locales/uk.json b/src/i18n/locales/uk.json index 70458c978..ba74e5fb9 100644 --- a/src/i18n/locales/uk.json +++ b/src/i18n/locales/uk.json @@ -1647,6 +1647,12 @@ "many": "{count} камер завантажено · увімкніть CCTV, щоб активувати", "other": "{count} камери завантажено · увімкніть CCTV, щоб активувати" }, + "{count} more cameras outside this view · pan or zoom out": { + "one": "Ще {count} камера поза цим видом · посуньте або віддаліть", + "few": "Ще {count} камери поза цим видом · посуньте або віддаліть", + "many": "Ще {count} камер поза цим видом · посуньте або віддаліть", + "other": "Ще {count} камери поза цим видом · посуньте або віддаліть" + }, "Enable CCTV to load camera intersections": "Увімкніть CCTV, щоб завантажити камери на перехрестях", "Enable CCTV to start camera-linked intelligence summaries.": "Увімкніть CCTV, щоб отримувати розвідувальні зведення за даними камер.", "No summary available.": "Зведення недоступне.", @@ -1989,10 +1995,6 @@ "STATION UNAVAILABLE": "СТАНЦІЯ НЕДОСТУПНА", "Station unavailable after directory refresh": "Станція недоступна після оновлення каталогу", "Radio could not {action} cleanly": "Радіо: не вдалося коректно {action}", - "Go to expanded Radio section": "Перейти до розгорнутого розділу радіо", - "Expand Radio section in Context": "Розгорнути розділ «Радіо» на панелі «Контекст»", - "Close compact Radio controls": "Закрити компактне керування радіо", - "Open compact Radio controls": "Відкрити компактне керування радіо", "SYNC": "СИНХР.", "RECONCILE": "ЗВІРИТИ", "DISABLE": "ВИМКНУТИ", @@ -2015,9 +2017,6 @@ "selected": "вибрана", "nearest": "найближча", "{action} {target} radio station": "{action}: {target} радіостанція", - "RADIO STATE UNCERTAIN": "СТАН РАДІО НЕВИЗНАЧЕНИЙ", - "SYNCING DIRECTORY": "СИНХРОНІЗАЦІЯ КАТАЛОГУ", - "RADIO READY": "РАДІО ГОТОВЕ", "SYNCING": "СИНХРОНІЗАЦІЯ", "READY": "ГОТОВО", "station": "станція", @@ -2864,11 +2863,6 @@ "RECENT IMAGERY": "СВІЖІ ЗНІМКИ", "Imagery days in the box": "Дні зі знімками в області", "CONTEXT": "КОНТЕКСТ", - "Compact Radio controls": "Компактне керування радіо", - "Open detailed Radio controls": "Відкрити докладне керування радіо", - "Previous station": "Попередня станція", - "Next station": "Наступна станція", - "Compact Radio volume": "Гучність компактного радіо", "Expand panel": "Розгорнути панель", "Context mode": "Режим контексту", "CONTACTS": "КОНТАКТИ", @@ -2929,7 +2923,6 @@ "Layout": "Компонування", "HUD layout": "Компонування HUD", "Tactical": "Тактичне", - "Operator": "Операторське", "Minimal": "Мінімальне", "Cyber": "Кібер", "Sonar": "Сонар", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index e030883fd..cf35f8254 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -1553,6 +1553,9 @@ "{count} cameras loaded · enable CCTV to activate": { "other": "已加载 {count} 个摄像头 · 启用 CCTV 以激活" }, + "{count} more cameras outside this view · pan or zoom out": { + "other": "此视图外还有 {count} 个摄像头 · 平移或缩小" + }, "Enable CCTV to load camera intersections": "启用 CCTV 以加载路口摄像头", "Enable CCTV to start camera-linked intelligence summaries.": "启用 CCTV 以开始生成与摄像头关联的情报摘要。", "No summary available.": "暂无摘要。", @@ -1861,10 +1864,6 @@ "STATION UNAVAILABLE": "电台不可用", "Station unavailable after directory refresh": "目录刷新后该电台不可用", "Radio could not {action} cleanly": "电台未能正常{action}", - "Go to expanded Radio section": "前往展开的电台区域", - "Expand Radio section in Context": "在态势面板中展开电台区域", - "Close compact Radio controls": "关闭精简电台控件", - "Open compact Radio controls": "打开精简电台控件", "SYNC": "同步", "RECONCILE": "核对", "DISABLE": "停用", @@ -1886,9 +1885,6 @@ "selected": "所选", "nearest": "最近", "{action} {target} radio station": "{action}{target}电台", - "RADIO STATE UNCERTAIN": "电台状态不确定", - "SYNCING DIRECTORY": "正在同步目录", - "RADIO READY": "电台就绪", "SYNCING": "同步中", "READY": "就绪", "station": "电台", @@ -2551,11 +2547,6 @@ "RECENT IMAGERY": "近期影像", "Imagery days in the box": "视野内的影像天数", "CONTEXT": "态势", - "Compact Radio controls": "精简电台控件", - "Open detailed Radio controls": "打开详细电台控件", - "Previous station": "上一个电台", - "Next station": "下一个电台", - "Compact Radio volume": "精简电台音量", "Expand panel": "展开面板", "Context mode": "态势模式", "CONTACTS": "目标", @@ -2616,7 +2607,6 @@ "Layout": "布局", "HUD layout": "HUD 布局", "Tactical": "战术", - "Operator": "操作员", "Minimal": "精简", "Cyber": "赛博", "Sonar": "声呐", diff --git a/src/i18n/locales/zh-Hant.json b/src/i18n/locales/zh-Hant.json index 5072a6678..58f8997be 100644 --- a/src/i18n/locales/zh-Hant.json +++ b/src/i18n/locales/zh-Hant.json @@ -1543,6 +1543,9 @@ "{count} cameras loaded · enable CCTV to activate": { "other": "已加載 {count} 個攝像頭 · 啟用 CCTV 以激活" }, + "{count} more cameras outside this view · pan or zoom out": { + "other": "此視圖外還有 {count} 個攝像頭 · 平移或縮小" + }, "Enable CCTV to load camera intersections": "啟用 CCTV 以加載路口攝像頭", "Enable CCTV to start camera-linked intelligence summaries.": "啟用 CCTV 以開始生成與攝像頭關聯的情報摘要。", "No summary available.": "暫無摘要。", @@ -1849,10 +1852,6 @@ "STATION UNAVAILABLE": "電臺不可用", "Station unavailable after directory refresh": "目錄刷新後該電臺不可用", "Radio could not {action} cleanly": "電臺未能正常{action}", - "Go to expanded Radio section": "前往展開的電臺區域", - "Expand Radio section in Context": "在態勢面板中展開電臺區域", - "Close compact Radio controls": "關閉精簡電臺控件", - "Open compact Radio controls": "打開精簡電臺控件", "SYNC": "同步", "RECONCILE": "核對", "DISABLE": "停用", @@ -1874,9 +1873,6 @@ "selected": "所選", "nearest": "最近", "{action} {target} radio station": "{action}{target}電臺", - "RADIO STATE UNCERTAIN": "電臺狀態不確定", - "SYNCING DIRECTORY": "正在同步目錄", - "RADIO READY": "電臺就緒", "SYNCING": "同步中", "READY": "就緒", "station": "電臺", @@ -2539,11 +2535,6 @@ "RECENT IMAGERY": "近期影像", "Imagery days in the box": "視野內的影像天數", "CONTEXT": "態勢", - "Compact Radio controls": "精簡電臺控件", - "Open detailed Radio controls": "打開詳細電臺控件", - "Previous station": "上一個電臺", - "Next station": "下一個電臺", - "Compact Radio volume": "精簡電臺音量", "Expand panel": "展開面板", "Context mode": "態勢模式", "CONTACTS": "目標", @@ -2604,7 +2595,6 @@ "Layout": "佈局", "HUD layout": "HUD 佈局", "Tactical": "戰術", - "Operator": "操作員", "Minimal": "精簡", "Cyber": "賽博", "Sonar": "聲吶", diff --git a/src/i18n/same-as-english.json b/src/i18n/same-as-english.json index 6dba5c211..dd0df9990 100644 --- a/src/i18n/same-as-english.json +++ b/src/i18n/same-as-english.json @@ -1,4 +1,170 @@ { + "de": [ + "1090 MHz", + "978 MHz UAT", + "AIS: --", + "AIS: {name}", + "ANIME", + "AUTO", + "Alternative", + "Ambient", + "Anime", + "Austin", + "BRG {value}°", + "BRG —", + "Bloom", + "Blues", + "Bus", + "CCTV", + "COCKPIT", + "CRS {value}°", + "CRT", + "Country", + "Cyan", + "Cyber", + "DEST ---°", + "DEST {bearing}", + "Dance", + "Details", + "Dubai", + "EXPORT", + "EXPORT {slot}", + "FALLBACK", + "FILTER", + "FLIR", + "FOV", + "FOV {value}°", + "FT", + "Folk", + "Funk", + "GEO", + "GOOGLE STREET VIEW", + "Global", + "Global · 60°S–60°N", + "Google Street View", + "HDG", + "HDG {value}°", + "Hip-Hop", + "House", + "Hybrid", + "Indie", + "Ironbow", + "Jazz", + "LIVE", + "Landsat 8/9", + "Latin", + "Layout", + "London", + "Los Angeles", + "MIL", + "MINI", + "MISSION", + "MM", + "MONITOR", + "Metal", + "Minimal", + "NAME", + "NASA FIRMS · LIVE", + "NAV", + "NEWS", + "NNW", + "NOIR", + "NORMAL", + "NVG", + "NW", + "New York", + "Noir", + "Normal", + "OK", + "ORBIT", + "ORBIT · ", + "Oldies", + "OpenStreetMap + optional Google Maps Places", + "Operator", + "PAUSE", + "PLAY", + "POS: LIVE", + "POS: {time}Z", + "POSITION {age}", + "PROJ MONITOR", + "Paris", + "Pause", + "Pin", + "Plan", + "Pop", + "Punk", + "RADIO", + "REC", + "REGION", + "RESET", + "Radio", + "Reggae", + "Rock", + "S2 GhostMaps", + "SAR", + "SIG", + "SNAPSHOT", + "SQUAWK {code} · {status}", + "SSW", + "STANDARD", + "START", + "STATION", + "STATUS", + "STATUS · ", + "STD", + "SW", + "SYNC", + "San Francisco", + "Scanlines", + "Scope", + "Sentinel-2", + "Sonar", + "Soul", + "Station", + "Station {id}", + "T minus {seconds}", + "TAGS · {tags}", + "TANKER", + "TEMP", + "Talk", + "Tallinn", + "Techno", + "Trance", + "Turboprop", + "UPSTREAM", + "Upstream HTTP {status}", + "Vignette", + "WHOT/BHOT", + "WIND", + "WNW", + "WSW", + "Washington DC", + "Wind", + "dB", + "feed status|live", + "fire behavior|Minimal", + "ft", + "hPa", + "km", + "km/h", + "min", + "mph", + "radio|NEWS", + "radio|TALK", + "view band|GLOBAL", + "{city} CCTV", + "{county} County", + "{source} HTTP {status}", + "{speed} kn", + "{type} SPD: {speed} HDG: {heading}", + "{value} KM", + "{value} KM DOWNRANGE", + "{value} KM/H", + "{value} KM/S", + "ΔN", + "Ⅱ PAUSE", + "▲ {frp} MW" + ], "es": [ "1090 MHz", "978 MHz UAT", @@ -126,6 +292,185 @@ "ΔN", "▲ {frp} MW" ], + "fr": [ + "1090 MHz", + "978 MHz UAT", + "ALTITUDE", + "ALTITUDE · FT", + "ANIME", + "AUTO", + "Action", + "Allocation", + "Altitude {altitude}", + "Ambient", + "Anime", + "Austin", + "Aviation / Marine", + "BRG {value}°", + "BRG —", + "Bloom", + "Blues", + "Bus", + "CAL {value}", + "CAL · {badge}", + "CALIBRATION", + "CARGO", + "CAUSE", + "CCTV", + "CCTV OFF", + "CCTV ON", + "COCKPIT", + "COMMERCIAL", + "CONTACT", + "CONTACTS", + "CONTACTS · 250 KM", + "CRS {value}°", + "CRT", + "Country", + "Cyan", + "Cyber", + "Cyclones · NHC / CPHC", + "DENSE", + "DENSE ···", + "DENSE ✕", + "DEST ---°", + "DEST {bearing}", + "DESTINATION", + "Dance", + "Drone", + "ENE", + "ESE", + "EUROPE", + "FLIR", + "FM MHZ", + "FOV", + "FOV {value}°", + "FT", + "Ferry", + "Folk", + "Funk", + "GAIN", + "GEO", + "GOOGLE STREET VIEW", + "Gain", + "Google Street View", + "Grain", + "HDG", + "HDG {value}°", + "Hip-Hop", + "House", + "IMAGE", + "Indie", + "Ironbow", + "Jazz", + "LOCAL", + "Landsat 8/9", + "Landsat 8/9 via HLS", + "Los Angeles", + "MIC", + "MIL", + "MINI", + "MISSION", + "MM", + "MSG/S", + "Metal", + "Minimal", + "Minutes", + "Mode", + "NAV", + "NE", + "NNE", + "NOIR", + "NORMAL", + "NVG", + "New York", + "Noir", + "Normal", + "OFF", + "OK", + "ON", + "ON/OFF", + "Oldies", + "PAUSE", + "POSITION {age}", + "PROJ OFF", + "PROJECTION OFF", + "PROJECTION ON", + "PROPAGATION", + "Paris", + "Pause", + "Plan", + "Pop", + "Punk", + "RADIO", + "REC", + "Radio", + "Reggae", + "Rock", + "S2 GhostMaps", + "SAR", + "SE", + "SIG", + "SOURCE", + "SQUAWK {code} · {status}", + "SRC {value}", + "SSE", + "STANDARD", + "STATION", + "STD", + "San Francisco", + "Satellites", + "Saturation", + "Sonar", + "Soul", + "Source", + "Station", + "Station {id}", + "Style", + "TAGS · {tags}", + "TEMP", + "TYPE", + "Tallinn", + "Techno", + "Tokyo", + "Train", + "Trance", + "USB {rate} msg/s", + "VOLUME", + "VS", + "WHOT/BHOT", + "audio {state}", + "dB", + "ft", + "hPa", + "image", + "installation class|BASE", + "installation class|INSTALLATION", + "km", + "km/h", + "max {value} MW", + "min", + "mini", + "mph", + "nominal", + "satellite", + "standard", + "station", + "voice|OFF", + "{count} MSGS", + "{count} SOURCES", + "{count} messages", + "{n} cyclones", + "{n} satellites", + "{rate} msg/s", + "{value} KM", + "{value} KM/H", + "{value} KM/S", + "ΔE", + "ΔN", + "Ⅱ PAUSE", + "▲ {frp} MW" + ], "it": [ "1090 MHz", "978 MHz UAT", @@ -296,6 +641,55 @@ "ΔN", "▲ {frp} MW" ], + "ja": [ + "1090 MHz", + "978 MHz UAT", + "AIS: --", + "AIS: {name}", + "BRG {value}°", + "BRG —", + "CAL {value}", + "CAL · {badge}", + "CCTV", + "CH {index} / {total}", + "CRS {value}°", + "CRT", + "FLIR", + "FLT", + "FM MHZ", + "FOV", + "FOV {value}°", + "FT", + "GEO", + "HDG", + "HDG {value}°", + "Ironbow", + "Landsat 8/9", + "MSG/S", + "NVG", + "POS: {time}Z", + "REC", + "S2 GhostMaps", + "SRC {value}", + "Sentinel-2", + "USB {rate} msg/s", + "WHOT/BHOT", + "dB", + "ft", + "hPa", + "km", + "km/h", + "kt", + "mph", + "{city} CCTV", + "{rate} msg/s", + "{source} HTTP {status}", + "{speed} kn", + "{type} SPD: {speed} HDG: {heading}", + "ΔE", + "ΔN", + "▲ {frp} MW" + ], "pt": [ "1090 MHz", "978 MHz UAT", diff --git a/src/i18n/zh-CN-display.test.mjs b/src/i18n/zh-CN-display.test.mjs index b9e55058d..29cccd44f 100644 --- a/src/i18n/zh-CN-display.test.mjs +++ b/src/i18n/zh-CN-display.test.mjs @@ -17,6 +17,9 @@ test('Chinese HUD and cockpit units keep their case without spacing coordinates' assert.equal(formatCockpitDistance(14000), '14\u00a0km'); assert.equal(LOCALIZED_LOCALITY.sector({ latDeg: 21.33, lonDeg: -77.5 }), '扇区 21.33N 77.50W'); + setLocale('ja'); + assert.equal(hudQuantity(13, 'km'), '13 km'); + assert.equal(hudQuantity(1.5, 'km', 1), '1.5 km'); setLocale('en'); assert.equal(hudQuantity(0, 'km'), '0KM'); assert.equal(hudQuantity(2069, 'km', 1), '2069.0KM'); @@ -88,7 +91,6 @@ test('radio option text and accessible name use the same localized parentheses', setLocale(locale); const host = { _radioPanel: element(), _radioFilter: element(), - _syncContextRadioLauncherState() {}, actions: { getLifecycle: () => null, scheduleLayout() {} }, }; renderRadioState.call(host, { diff --git a/src/keySetup.test.mjs b/src/keySetup.test.mjs index e7e6c70ae..013dbbd87 100644 --- a/src/keySetup.test.mjs +++ b/src/keySetup.test.mjs @@ -48,7 +48,7 @@ test('setup placement moves the same button only in Cyber and disconnects on tea chip, ); assert.equal(chip.parentNode, home); - for (const variant of ['cyber', 'operator', 'cyber', 'minimal']) { + for (const variant of ['cyber', 'tactical', 'cyber', 'minimal']) { theme.dataset.uiTheme = variant; notify(); assert.equal(chip.parentNode, variant === 'cyber' ? toolbar : home); diff --git a/src/layers/cctv/cards.js b/src/layers/cctv/cards.js index 72f0b88a4..04b8fb86f 100644 --- a/src/layers/cctv/cards.js +++ b/src/layers/cctv/cards.js @@ -99,7 +99,8 @@ export function createCards({ state: layerState, services, parts, source }) { const candidates = []; const screenById = new Map(); - for (const record of layerState._records) { + // Cards go only on the icons the view pass shows (refreshHorizonCulling). + for (const record of layerState._viewRecords) { const id = record.camera.id; if (id === activeId || !record.position) continue; let inView = false; @@ -187,7 +188,14 @@ export function createCards({ state: layerState, services, parts, source }) { nowMs: Date.now(), cardLimit: budgets.cardLimit, }); - layerState._cardIds = new Set(retention.keepIds); + // Grace keeps a card briefly after it loses selection, but never on a + // camera whose icon the view pass has hidden. + const shownIds = new Set( + layerState._viewRecords.map((record) => record.camera.id), + ); + layerState._cardIds = new Set( + retention.keepIds.filter((id) => shownIds.has(id)), + ); layerState._cardGraceState = retention.graceState; // Bounded thumbnail LRU: live cards (grace included) never lose their @@ -371,7 +379,6 @@ export function createCards({ state: layerState, services, parts, source }) { layerState._cctvOverlayHost.clearSource(CCTV_OVERLAY_SOURCE_ID); layerState._cctvOverlayHost.setVisible(CCTV_OVERLAY_SOURCE_ID, false); parts.hover.clearHoverCard(); - layerState._cameraMoving = false; layerState._cardIds = new Set(); layerState._cardGraceState = new Map(); layerState._cardFrameSlots = new Map(); diff --git a/src/layers/cctv/controls.js b/src/layers/cctv/controls.js index af9f1f427..492ff1758 100644 --- a/src/layers/cctv/controls.js +++ b/src/layers/cctv/controls.js @@ -205,22 +205,21 @@ export function createControls({ state: layerState, services, parts, source }) { * @returns {{ position: Cesium.Cartesian3, id: string, type: string }[]} */ getDetectableObjects(options = {}) { - if (!layerState._enabled || layerState._records.length === 0) return []; + // The cameras the view pass shows, not icons it has hidden. + const records = layerState._viewRecords; + if (!layerState._enabled || records.length === 0) return []; const maxCount = Number.isFinite(options.maxCount) ? Math.max(1, Math.floor(options.maxCount)) - : layerState._records.length; + : records.length; const seed = Number.isFinite(options.seed) ? Math.floor(options.seed) : 0; - const stride = Math.max( - 1, - Math.ceil(layerState._records.length / maxCount), - ); + const stride = Math.max(1, Math.ceil(records.length / maxCount)); const start = seed % stride; const objects = []; - for (let i = start; i < layerState._records.length; i += stride) { - const camera = layerState._records[i].camera; + for (let i = start; i < records.length; i += stride) { + const camera = records[i].camera; objects.push({ - position: layerState._records[i].position, + position: records[i].position, sourceId: camera.id, // Short semantic code where the id is opaque (see cameraDisplayCode). id: `CAM-${camera.code || camera.id}`, diff --git a/src/layers/cctv/geometry.js b/src/layers/cctv/geometry.js index d9db40fe9..362ad4789 100644 --- a/src/layers/cctv/geometry.js +++ b/src/layers/cctv/geometry.js @@ -581,6 +581,13 @@ export function createGeometry({ state: layerState, services, parts, source }) { function coverageNeighborCount(targetRecord) { if (!targetRecord) return 0; + // The summary asks on every notification; scan the catalog only when the + // camera, its pose or the catalog changed. + const { id, lat, lon, rangeM } = targetRecord.camera; + const key = `${id}|${lat}|${lon}|${rangeM}|${layerState._records.length}`; + const cached = layerState._neighborCountCache; + if (cached?.key === key && cached.records === layerState._records) + return cached.count; let count = 0; for (const record of layerState._records) { if (record === targetRecord) continue; @@ -594,6 +601,11 @@ export function createGeometry({ state: layerState, services, parts, source }) { ((targetRecord.camera.rangeM + record.camera.rangeM) / 1000) * 0.92; if (dKm <= overlapKm) count++; } + layerState._neighborCountCache = { + key, + records: layerState._records, + count, + }; return count; } diff --git a/src/layers/cctv/geometryQueue.js b/src/layers/cctv/geometryQueue.js index 5583e36af..b78950aaa 100644 --- a/src/layers/cctv/geometryQueue.js +++ b/src/layers/cctv/geometryQueue.js @@ -6,6 +6,8 @@ import { GEO_TRACKING_BATCH_DELAY_MS, GEO_LOAD_BATCH_SIZE, GEO_LOAD_BATCH_DELAY_MS, + VIEW_GEOMETRY_ATTEMPTS, + VIEW_GEOMETRY_RETRY_MS, } from './policy.js'; export function createGeometryQueue({ @@ -25,6 +27,8 @@ export function createGeometryQueue({ clearTimeout(layerState._geoQueueTimer); layerState._geoQueueTimer = 0; } + clearTimeout(layerState._viewGeometryRetryTimer); + layerState._viewGeometryRetryTimer = 0; layerState._geoQueue = []; layerState._geoProgressNotifier = null; if (clearProgress) { @@ -223,6 +227,7 @@ export function createGeometryQueue({ err?.message || err, ); } + noteViewAttempt(record); if ( layerState._geoLoading && layerState._geoLoadDone < layerState._geoLoadTotal @@ -236,14 +241,15 @@ export function createGeometryQueue({ layerState._geoLoading = false; if (wasInitialLoad) { layerState._geoLoadDone = layerState._geoLoadTotal; - if (layerState._enabled) { - parts.rendering.refreshCoverageStyles(); - // Geometry refinement may have replaced record.position objects — the - // one-shot drain completion re-anchors the card entries (event-driven, - // not a per-frame or timer pass). - parts.cards.refreshAmbientCards(); - } + if (layerState._enabled) parts.rendering.refreshCoverageStyles(); } + // Geometry refinement may have replaced record.position objects — every + // drain's completion re-anchors the card entries (event-driven, not a + // per-frame or timer pass). + // While the camera moves, moveEnd reselects them instead. + if (layerState._enabled && !layerState._cameraMoving) + parts.cards.refreshAmbientCards(); + scheduleViewGeometryRetry(); // Completion is never coalesced: subscribers must observe the final // loading state even if the last progress tick just happened. layerState._geoProgressNotifier?.finish(); @@ -261,16 +267,17 @@ export function createGeometryQueue({ /** * Appends records to the geometry queue (no progress tracking) and starts - * the batch timer if idle. Used by update()'s ONE-SHOT tiles-ready completion - * pass (records left `!groundResolved` by an enable-time drain that ran while - * tiles were still streaming) so it shares the same stagger machinery as the - * initial load. Fires at most once per enable — never on a recurring timer. + * the batch timer if idle. Used by update()'s one-shot tiles-ready + * completion pass and by enqueueViewGeometry, so both share the initial + * load's stagger machinery. Never driven by a recurring timer. * @param {Object[]} records - Camera records needing geometry refresh. */ function enqueueGeometryRefresh(records) { + const queued = new Set(layerState._geoQueue); for (const record of records) { - if (!layerState._geoQueue.includes(record)) { + if (!queued.has(record)) { + queued.add(record); layerState._geoQueue.push(record); } } @@ -283,10 +290,10 @@ export function createGeometryQueue({ } /** - * Starts the initial staggered load: orders all records active-camera-first, - * then by distance from the current viewer position (nearest first, so - * cameras likely in view refine before off-screen ones), and exposes - * loaded/total progress through uiState()/getStats() while running. + * Starts the initial staggered load over the geometry working set: active + * camera first, then by distance from the current viewer position (nearest + * first), and exposes loaded/total progress through uiState()/getStats() + * while running. Cameras outside the view refine when they come into it. */ function startGeometryLoadQueue() { @@ -303,7 +310,7 @@ export function createGeometryQueue({ const refLon = carto ? Cesium.Math.toDegrees(carto.longitude) : (active?.camera.lon ?? 0); - const pending = layerState._records + const pending = geometryWorkingSet() .filter((record) => record !== active) .map((record) => ({ record, @@ -325,7 +332,134 @@ export function createGeometryQueue({ ); layerState._geoQueueTimer = setTimeout(processGeometryBatch, 0); } + /** + * The records whose geometry the view needs: the shown icons, the active + * camera and the coverage cohort drawn around it. + * @returns {Object[]} + */ + + function geometryWorkingSet() { + const records = new Set(layerState._viewRecords); + const active = parts.selection.getActiveRecord(); + if (active) records.add(active); + for (const id of layerState._coverageVisibleIds) { + const record = layerState._recordById.get(id); + if (record) records.add(record); + } + return [...records]; + } + + /** The regime key a view attempt counts against: interim while tiles stream. */ + function viewAttemptKey() { + const regime = parts.ground.currentSurfaceRegime(); + const tilesReady = + regime !== 'google-3d' || parts.model.projectionTilesReady(); + return { key: tilesReady ? regime : `${regime}:interim`, tilesReady }; + } + + /** + * Counts a processed geometry attempt against the current regime key; each + * key keeps its own count, so tile streaming going on and off does not + * reset the ready-tiles budget. + */ + function noteViewAttempt(record) { + const { key } = viewAttemptKey(); + record.viewAttempts ??= {}; + record.viewAttempts[key] = (record.viewAttempts[key] || 0) + 1; + record.viewAttemptAt = performance.now(); + } + + /** + * Whether a record may be attempted again under `key`. After a first + * unresolved attempt, the second waits out VIEW_GEOMETRY_RETRY_MS so its + * elevation cell can warm, rather than going to the next motion tick. + */ + /** Whether a record still has an attempt left under the current key. */ + function withinViewBudget(record) { + const { key } = viewAttemptKey(); + return (record.viewAttempts?.[key] || 0) < VIEW_GEOMETRY_ATTEMPTS; + } + + function attemptsLeft(record, key) { + const attempts = record.viewAttempts?.[key] || 0; + if (attempts >= VIEW_GEOMETRY_ATTEMPTS) return false; + return ( + attempts === 0 || + performance.now() - record.viewAttemptAt >= VIEW_GEOMETRY_RETRY_MS + ); + } + + /** + * After a drain, gives working-set cameras it left ungrounded one more + * attempt once their elevation cells have had time to warm. The timer is + * set for the earliest such camera and re-arms for the rest when it fires. + */ + function scheduleViewGeometryRetry() { + if (!layerState._enabled || layerState._viewGeometryRetryTimer) return; + const { key } = viewAttemptKey(); + const active = parts.selection.getActiveRecord(); + let due = Infinity; + for (const record of geometryWorkingSet()) { + const attempts = record.viewAttempts?.[key] || 0; + if ( + record === active || + attempts === 0 || + attempts >= VIEW_GEOMETRY_ATTEMPTS || + !parts.ground.needsGeometry(record) + ) + continue; + due = Math.min(due, record.viewAttemptAt + VIEW_GEOMETRY_RETRY_MS); + } + if (due === Infinity) return; + layerState._viewGeometryRetryTimer = setTimeout( + () => { + layerState._viewGeometryRetryTimer = 0; + enqueueViewGeometry(); + // A drain re-arms on completion; with none running, the cameras still + // cooling down need their own timer. + if (!layerState._geoQueueTimer) scheduleViewGeometryRetry(); + }, + Math.max(0, due - performance.now()), + ); + } + + /** + * Queues working-set cameras the current surface has not grounded yet, + * after a camera move brings new ones into view. Attempts are counted when + * a record is processed, not queued, so a drain cut short by disable() is + * not lost; each record gets VIEW_GEOMETRY_ATTEMPTS per surface regime, + * spaced by VIEW_GEOMETRY_RETRY_MS, so a sample the shared floor refuses + * (too far, too high) is not retried on every move. While photoreal tiles are still streaming the attempts are + * interim, and the update() completion pass is re-armed to retry these + * records when they land. The active camera is left to selection, which + * owns its re-sampling. + */ + + function enqueueViewGeometry() { + if (!layerState._enabled) return; + const { key, tilesReady } = viewAttemptKey(); + const active = parts.selection.getActiveRecord(); + const pending = []; + let coolingDown = false; + for (const record of geometryWorkingSet()) { + if (record === active || !parts.ground.needsGeometry(record)) continue; + if (!attemptsLeft(record, key)) { + coolingDown ||= + (record.viewAttempts?.[key] || 0) < VIEW_GEOMETRY_ATTEMPTS; + continue; + } + pending.push(record); + } + // A camera back in view during its cooldown still needs its retry timer. + if (coolingDown && !layerState._geoQueueTimer) scheduleViewGeometryRetry(); + if (!pending.length) return; + if (!tilesReady) layerState._tilesReadyReenqueued = false; + enqueueGeometryRefresh(pending); + } return { + enqueueViewGeometry, + withinViewBudget, + geometryWorkingSet, stopGeometryLoadQueue, createGeometryProgressNotifier, processCctvGeometryQueueBatch, diff --git a/src/layers/cctv/ground.js b/src/layers/cctv/ground.js index 5ff41c377..fdf36db14 100644 --- a/src/layers/cctv/ground.js +++ b/src/layers/cctv/ground.js @@ -1,3 +1,4 @@ +import * as Cesium from 'cesium'; import { SUPPORT_KEYS, planeSupportPoints, @@ -78,6 +79,14 @@ export function createGround({ state: layerState, services, parts, source }) { return record?.groundResolved?.[regime] === true; } + /** + * True until the record has geometry grounded for the regime. Cameras are + * refined only once they enter the view, so many never have geometry. + */ + function needsGeometry(record, regime = currentSurfaceRegime()) { + return !record?.frustumGeometry || !isGroundResolved(record, regime); + } + /** * Ground altitude used for pure geometry recomputes: the given regime's * cached resolution (`record.groundSamples[regime]` — a shared mesh/DEM floor @@ -184,8 +193,11 @@ export function createGround({ state: layerState, services, parts, source }) { * Task 5: applies a LATE-arriving ground-prior batch (init's bounded race * lost — cold proxy cache / slow upstream). Pure recomputes only, no scene * queries: + * - records without geometry yet: only the icon moves onto the prior; + * the frustum is built when the camera enters the view. * - terrain-globe regime: the prior IS the resolution → re-run the - * resolution (updateRecordGeometry latches it) for every record. + * resolution (updateRecordGeometry latches it) for every record with + * geometry. * - google-3d regime: records still awaiting a shared floor move from the * catalog fallback onto the exact prior; records already holding a shared * mesh/DEM floor keep it untouched. @@ -198,6 +210,7 @@ export function createGround({ state: layerState, services, parts, source }) { function applyLateGroundPriors(records, priors) { if (!Array.isArray(records) || !Array.isArray(priors)) return; let applied = 0; + let moved = 0; for (let i = 0; i < records.length && i < priors.length; i++) { const record = records[i]; const prior = priors[i]; @@ -212,6 +225,24 @@ export function createGround({ state: layerState, services, parts, source }) { record.camera.absoluteHeightM = prior.ellipsoid + record.camera.mountHeightM; } + // Cameras without geometry only move their icon onto the prior, so the + // view pass projects the right point; their frustums are built when + // they enter the view, as recomputing the whole catalog here would + // stall a large one. + if (!record.frustumGeometry) { + if (record.position) { + Cesium.Cartesian3.fromDegrees( + record.camera.lon, + record.camera.lat, + record.camera.absoluteHeightM, + undefined, + record.position, + ); + if (record.billboard) record.billboard.position = record.position; + moved += 1; + } + continue; + } const regime = currentSurfaceRegime(); if (regime === 'terrain-globe') { // Prior IS the resolution — re-latch onto the fresh value. @@ -224,7 +255,9 @@ export function createGround({ state: layerState, services, parts, source }) { applied += 1; } } - if (applied) parts.presentation.notifyListeners(); + if (moved && parts.rendering.refreshHorizonCulling()) + parts.geometryQueue.enqueueViewGeometry(); + if (applied || moved) parts.presentation.notifyListeners(); } /** @@ -234,7 +267,7 @@ export function createGround({ state: layerState, services, parts, source }) { * deck/building-top in google-3d vs bare Re:Earth DEM on globe stacks), so * on a REGIME change (photoreal ↔ globe; bing→osm stays 'terrain-globe' and * no-ops): - * 1. every record's geometry recomputes IMMEDIATELY from the new regime's + * 1. every record that has geometry recomputes IMMEDIATELY from the new regime's * resolution — cached sample if that regime has one, else the Re:Earth * prior (never blank, zero scene queries); * 2. entering google-3d re-arms the one-shot tiles-ready completion latch so @@ -250,6 +283,9 @@ export function createGround({ state: layerState, services, parts, source }) { layerState._lastAppliedRegime = regime; for (const record of layerState._records) { + // Only cameras that already have geometry are rewritten; the rest are + // grounded for the new regime when they enter the view. + if (!record.frustumGeometry) continue; if (regime === 'terrain-globe') { // Prior IS the resolution — latch it (zero scene queries). record.groundSamples['terrain-globe'] = groundPriorAltFor(record); @@ -353,6 +389,7 @@ export function createGround({ state: layerState, services, parts, source }) { resolveFootprintGround, groundPriorAltFor, isGroundResolved, + needsGeometry, groundAltFor, rearmGroundResolution, resolveCommittedGroundAnchor, diff --git a/src/layers/cctv/index.js b/src/layers/cctv/index.js index 0deba580e..6f1cc77c7 100644 --- a/src/layers/cctv/index.js +++ b/src/layers/cctv/index.js @@ -91,6 +91,9 @@ export function createCctvLayer({ services, source }) { processGeometryBatch: parts.geometryQueue.processGeometryBatch, hideCctvRecordVisuals: parts.rendering.hideCctvRecordVisuals, refreshCoverageStyles: parts.rendering.refreshCoverageStyles, + refreshHorizonCulling: parts.rendering.refreshHorizonCulling, + _viewRecordIdsForTest: () => + state._viewRecords.map((record) => record.camera.id), clearProbeClampOnDeactivation: parts.geometry.clearProbeClampOnDeactivation, cctvRecordNeedsActivation: parts.model.cctvRecordNeedsActivation, diff --git a/src/layers/cctv/ingestion.js b/src/layers/cctv/ingestion.js index 0947ccdb6..2613de1e1 100644 --- a/src/layers/cctv/ingestion.js +++ b/src/layers/cctv/ingestion.js @@ -32,13 +32,22 @@ export function createIngestion({ // projectionTilesReady() is false while a (hidden) Google tileset // exists, so this latch effectively fires for the google-3d regime — // terrain-globe records resolve from the prior in their drain pass. - const unresolved = layerState._records.filter( - (record) => !parts.ground.isGroundResolved(record), - ); + const unresolved = parts.geometryQueue + .geometryWorkingSet() + .filter( + (record) => + !parts.ground.isGroundResolved(record) && + parts.geometryQueue.withinViewBudget(record), + ); if (unresolved.length) parts.geometryQueue.enqueueGeometryRefresh(unresolved); } await parts.health.syncHealthState(); + // New health can change which camera wins a crowded cell. + if (layerState._enabled && parts.rendering.refreshHorizonCulling()) { + parts.geometryQueue.enqueueViewGeometry(); + parts.cards.refreshAmbientCards(); + } parts.navigation.maybeAutoHop(now); parts.presentation.notifyListeners(); }, diff --git a/src/layers/cctv/lifecycle.js b/src/layers/cctv/lifecycle.js index 6d041aa78..4e4443df8 100644 --- a/src/layers/cctv/lifecycle.js +++ b/src/layers/cctv/lifecycle.js @@ -7,8 +7,14 @@ import { CAMERA_ICON, IDLE_CAMERA_COLOR, CALIBRATION_RANGE_FLOOR_M, + VIEW_MOTION_REFRESH_MS, + VIEW_MOTION_MIN_MOVE_FRACTION, + VIEW_MOTION_MIN_TURN_DEG, + VIEW_MOTION_COST_FACTOR, } from './policy.js'; +const MIN_TURN_COS = Math.cos((VIEW_MOTION_MIN_TURN_DEG * Math.PI) / 180); + export function createLifecycle({ state: layerState, services, @@ -22,15 +28,94 @@ export function createLifecycle({ const { onFocusTargetAppear } = services.focus; const { releaseContinuousRender } = services.render; + /** + * The settled-view pass: thin the icons, queue the newly visible geometry, + * reselect cards, and tell subscribers (the camera picker) when the shown + * set changed since they were last told, including by motion passes. + */ + function settleView() { + const changed = parts.rendering.refreshHorizonCulling(); + noteViewPose(); + parts.geometryQueue.enqueueViewGeometry(); + parts.cards.refreshAmbientCards(); + if (changed || layerState._viewUnpublished) { + layerState._viewUnpublished = false; + parts.presentation.notifyListeners(); + } + } + + /** Records the camera pose the last view pass ran at. */ + function noteViewPose() { + const camera = layerState._viewer?.camera; + if (!camera?.positionWC) return; + layerState._viewPose = { + position: Cesium.Cartesian3.clone(camera.positionWC), + direction: + camera.directionWC && Cesium.Cartesian3.clone(camera.directionWC), + }; + } + + /** + * Whether the camera moved or turned enough since the last view pass to + * change what is on screen. A camera that tracks or flies without settling + * would otherwise rerun the full pass on every tick. + */ + function viewPoseChanged() { + const camera = layerState._viewer?.camera; + const pose = layerState._viewPose; + if (!pose || !camera?.positionWC) return true; + const height = Math.max(1, camera.positionCartographic?.height ?? 0); + const moved = Cesium.Cartesian3.distance(camera.positionWC, pose.position); + if (moved > height * VIEW_MOTION_MIN_MOVE_FRACTION) return true; + if (!pose.direction || !camera.directionWC) return false; + return ( + Cesium.Cartesian3.dot(camera.directionWC, pose.direction) < MIN_TURN_COS + ); + } + + function viewMotionTick() { + if (!layerState._enabled) return; + const now = performance.now(); + const pass = layerState._viewMotionPass; + if (pass && now - pass.at < pass.ms * VIEW_MOTION_COST_FACTOR) return; + if (!viewPoseChanged()) return; + if (parts.rendering.refreshHorizonCulling()) + layerState._viewUnpublished = true; + noteViewPose(); + parts.geometryQueue.enqueueViewGeometry(); + layerState._viewMotionPass = { at: now, ms: performance.now() - now }; + } + + function startViewMotionTimer() { + if (layerState._viewMotionTimer) return; + layerState._viewMotionTimer = setInterval( + viewMotionTick, + VIEW_MOTION_REFRESH_MS, + ); + } + + function stopViewMotionTimer() { + clearInterval(layerState._viewMotionTimer); + layerState._viewMotionTimer = 0; + } + /** Resets all module-scoped runtime state to initial values. */ function clearRuntimeState() { parts.geometryQueue.stopGeometryLoadQueue(); + stopViewMotionTimer(); // Idempotent — also covers a re-init without a prior destroy(). parts.cards.teardownAmbientCards(); parts.projection.clearProjectionOverlay(); layerState._records = []; layerState._recordById = new Map(); + layerState._viewRecords = []; + layerState._cameraIndexCache = null; + layerState._coverageVisibleIds = new Set(); + layerState._neighborCountCache = null; + layerState._viewUnpublished = false; + layerState._viewPose = null; + layerState._viewMotionPass = null; layerState._healthById = new Map(); layerState._count = 0; layerState._lastUpdate = null; @@ -71,6 +156,13 @@ export function createLifecycle({ layerState._calibrationById = parts.calibration.loadCalibrationStore(); layerState._billboards = new Cesium.BillboardCollection(); + // Cesium switches a property's buffer-usage hint whenever it starts or + // stops changing from one frame to the next, and every switch rewrites + // the vertex data of every billboard. The view pass toggles `show` on a + // few icons of a large catalog at a time, so on a 33K catalog each pass + // cost two full rewrites. With the hint fixed, Cesium uploads only the + // icons that changed. + layerState._billboards.computeNewBuffersUsage = () => false; layerState._viewer.scene.primitives.add(layerState._billboards); registerSpriteCollection('cctv', layerState._billboards); @@ -243,28 +335,76 @@ export function createLifecycle({ // Field-test fix (2026-07-06): horizon-cull on camera settle (pairs with // the billboards' always-on-top depth setting) + one initial pass so the // first paint is already culled. + // A re-init against a new viewer moves the move listeners to its camera. + if ( + layerState._viewListenerCamera && + layerState._viewListenerCamera !== layerState._viewer.camera + ) { + const previous = layerState._viewListenerCamera; + if (layerState._horizonCullListener) + previous.moveEnd?.removeEventListener( + layerState._horizonCullListener, + ); + if (layerState._moveStartListener) + previous.moveStart?.removeEventListener( + layerState._moveStartListener, + ); + layerState._horizonCullListener = null; + layerState._moveStartListener = null; + layerState._cameraMoving = false; + } + layerState._viewListenerCamera = layerState._viewer.camera; if (!layerState._horizonCullListener) { // Ambient cards piggyback the same settle event: moveEnd-driven // reselection only, never per frame (refreshAmbientCards no-ops while // the layer is disabled). layerState._horizonCullListener = () => { layerState._cameraMoving = false; - parts.rendering.refreshHorizonCulling(); - parts.cards.refreshAmbientCards(); + stopViewMotionTimer(); + settleView(); }; layerState._viewer.camera.moveEnd.addEventListener( layerState._horizonCullListener, ); } if (!layerState._moveStartListener) { - // Item B: hover picking pauses while the camera is in motion. + // Item B: hover picking pauses while the camera is in motion. A long + // move (cockpit flight, a drag) never settles, so the view pass also + // reruns on an interval until it does. layerState._moveStartListener = () => { layerState._cameraMoving = true; + // enable() starts it for a move already under way. + if (layerState._enabled) startViewMotionTimer(); }; layerState._viewer.camera.moveStart.addEventListener( layerState._moveStartListener, ); } + // A resize or rotation changes what is on screen without moving the + // camera. + const canvas = layerState._viewer.scene?.canvas; + if (layerState._viewResizeCanvas !== canvas) { + // A re-init against a new viewer observes its canvas instead. + layerState._viewResizeObserver?.disconnect(); + layerState._viewResizeObserver = null; + layerState._viewResizeCanvas = null; + } + if ( + !layerState._viewResizeObserver && + typeof ResizeObserver === 'function' && + canvas + ) { + let frame = 0; + layerState._viewResizeObserver = new ResizeObserver(() => { + if (frame || !layerState._enabled) return; + frame = requestAnimationFrame(() => { + frame = 0; + if (layerState._enabled) settleView(); + }); + }); + layerState._viewResizeObserver.observe(canvas); + layerState._viewResizeCanvas = canvas; + } parts.rendering.refreshHorizonCulling(); layerState._clickHandler = new Cesium.ScreenSpaceEventHandler( @@ -325,6 +465,12 @@ export function createLifecycle({ ); await parts.health.syncHealthState(true); + // Health ranks crowded cells: thin again now that it has arrived (a + // no-op unless enable() ran while health was loading). + if (parts.rendering.refreshHorizonCulling()) { + parts.geometryQueue.enqueueViewGeometry(); + parts.cards.refreshAmbientCards(); + } parts.rendering.refreshCoverageStyles(); parts.presentation.notifyListeners(); restoreSpriteOrder(layerState._viewer); @@ -370,6 +516,9 @@ export function createLifecycle({ parts.projection.ensureProjectionRuntime(activeRecord); parts.frames.refreshProjectionImage(activeRecord, true); } + parts.rendering.refreshHorizonCulling(); + // Re-enabled mid-flight: moveStart has already fired. + if (layerState._cameraMoving) startViewMotionTimer(); parts.geometryQueue.startGeometryLoadQueue(); parts.rendering.refreshCoverageStyles(); parts.projection.startProjectionLoop(); @@ -402,10 +551,14 @@ export function createLifecycle({ layerState._removeFocusAppearListener = null; parts.projection.stopProjectionLoop(); parts.geometryQueue.stopGeometryLoadQueue(); + stopViewMotionTimer(); // Ambient cards tear down COMPLETELY on disable (owner design point 6): // source entries, pacer timer, in-flight handlers, and caches. parts.cards.teardownAmbientCards(); parts.rendering.hideCctvVisuals(); + // Nothing is in view while the layer is off. + layerState._viewRecords = []; + layerState._cameraIndexCache = null; parts.presentation.notifyListeners(); }, @@ -443,6 +596,13 @@ export function createLifecycle({ ); layerState._moveStartListener = null; } + // The move listeners are gone; nothing will report the end of a move. + layerState._cameraMoving = false; + layerState._viewListenerCamera = null; + stopViewMotionTimer(); + layerState._viewResizeObserver?.disconnect(); + layerState._viewResizeObserver = null; + layerState._viewResizeCanvas = null; if (layerState._gizmo) { layerState._gizmo.destroy(); layerState._gizmo = null; diff --git a/src/layers/cctv/policy.js b/src/layers/cctv/policy.js index bc369c02a..574f87527 100644 --- a/src/layers/cctv/policy.js +++ b/src/layers/cctv/policy.js @@ -60,6 +60,45 @@ export const GEO_PROGRESS_NOTIFY_INTERVAL_MS = 300; export const GEO_PROGRESS_NOTIFY_BATCH_LIMIT = 10; +// View thinning: camera icons show only on or near the screen, and each +// camera competes for a world-anchored grid cell sized to about VIEW_CELL_PX +// at its distance from the viewer (in power-of-two metre steps, so a pan +// keeps the same winners until a camera's distance crosses a step). Cells +// smaller than VIEW_MIN_CELL_M do not thin: +// close up every camera shows, co-located ones included. + +export const VIEW_CELL_PX = 40; + +export const VIEW_MIN_CELL_M = 60; + +// Icons this far past the screen edge (fraction of its size) still show, so +// a pan does not reveal empty ground before the next view pass. + +export const VIEW_ICON_MARGIN = 0.5; + +// While the camera moves, the view pass reruns at most this often. + +export const VIEW_MOTION_REFRESH_MS = 250; +// A motion tick is skipped unless the camera moved this fraction of its +// ellipsoid height, or turned this many degrees, since the last pass. + +export const VIEW_MOTION_MIN_MOVE_FRACTION = 0.01; + +export const VIEW_MOTION_MIN_TURN_DEG = 0.5; + +// Motion passes are spaced at least this many times the last pass's cost, so +// a large catalog spends at most about a tenth of a long move on them. + +export const VIEW_MOTION_COST_FACTOR = 10; + +// A view camera's geometry is attempted at most this many times per surface +// regime (a cold elevation cell can leave the first attempt unresolved); the +// retry runs this long after the drain that left it unresolved. + +export const VIEW_GEOMETRY_ATTEMPTS = 2; + +export const VIEW_GEOMETRY_RETRY_MS = 1500; + // Throttle for placeholder repaints — the projection RAF loop must not // re-fill a 1080p canvas on every frame while a feed image is still loading. diff --git a/src/layers/cctv/presentation.i18n.test.mjs b/src/layers/cctv/presentation.i18n.test.mjs new file mode 100644 index 000000000..4bfa5186b --- /dev/null +++ b/src/layers/cctv/presentation.i18n.test.mjs @@ -0,0 +1,58 @@ +// The CCTV HUD summary shows the CAL badge, source kind and view band in the +// UI language; English keeps its codes, and unknown source codes stay codes. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { setLocale } from '../../i18n/index.js'; +import { createPresentation } from './presentation.js'; + +function summary(sourceKind) { + const camera = { + id: 'c1', + city: 'Austin', + name: 'Cam', + rangeM: 100, + fovDeg: 60, + feedType: 'image', + }; + const record = { camera }; + const presentation = createPresentation({ + state: { + _records: [record], + _healthById: new Map(sourceKind ? [['c1', { sourceKind }]] : []), + _showProjection: false, + _coverageMode: 'sector', + }, + services: {}, + parts: { + selection: { getActiveRecord: () => record }, + model: { + sectorAreaKm2: () => 1, + currentViewContext: () => 'street:1:2', + }, + geometry: { coverageNeighborCount: () => 0 }, + calibration: { deriveCalBadge: () => 'raw-prior' }, + }, + source: {}, + }); + return presentation.buildSummaryText(); +} + +test('en: CAL, SRC and CONTEXT keep their English codes', () => { + setLocale('en'); + assert.match(summary('upstream'), /CAL RAW PRIOR · SRC UPSTREAM/); + assert.match(summary(null), /SRC IMAGE · STREET CONTEXT/); + assert.match(summary('upstream-image'), /SRC UPSTREAM-IMAGE/); +}); + +test('fr: CAL, SRC and CONTEXT values are translated', () => { + setLocale('fr'); + try { + assert.match( + summary('upstream'), + /CAL ESTIM\. INITIALE · SRC AMONT · CONTEXTE RUE/, + ); + assert.match(summary('upstream-image'), /SRC UPSTREAM-IMAGE/); + } finally { + setLocale('en'); + } +}); diff --git a/src/layers/cctv/presentation.js b/src/layers/cctv/presentation.js index 898e4605c..16c67aa4b 100644 --- a/src/layers/cctv/presentation.js +++ b/src/layers/cctv/presentation.js @@ -274,14 +274,53 @@ export function createPresentation({ }, activeCameraId: activeId, activeCamera: active ? getPublicCameraState(active, activeId) : null, - cameras: layerState._records.map((record) => - getPublicCameraState(record, activeId), - ), + cameraIndex: cameraIndex(), summary: buildSummaryText(), }; + // The full per-camera state is built only when a subscriber reads it: a + // catalog of tens of thousands would otherwise be rebuilt on every + // progress tick for listeners that only want the active camera. It reads + // the records live at that moment, and spreading or serializing the + // payload builds it, so callers that want a count use `count`. + let cameras = null; + Object.defineProperty(payload, 'cameras', { + enumerable: true, + get() { + cameras ??= layerState._records.map((record) => + getPublicCameraState(record, activeId), + ); + return cameras; + }, + }); return payload; } + /** + * `{ id, city, name }` for the active camera, then every camera the view + * pass shows: what the camera picker lists. A whole catalog would put tens + * of thousands of options in the DOM and slow every layout. The same array + * is returned until the view or the active camera changes, so a consumer + * can compare it by identity. + * @returns {{id: string, city: string, name: string}[]} + */ + + function cameraIndex() { + const view = layerState._viewRecords; + const active = parts.selection.getActiveRecord(); + const cached = layerState._cameraIndexCache; + if (cached?.view === view && cached.active === active) return cached.list; + const records = active + ? [active, ...view.filter((record) => record !== active)] + : view; + const list = records.map(({ camera }) => ({ + id: camera.id, + city: camera.city, + name: camera.name, + })); + layerState._cameraIndexCache = { view, active, list }; + return list; + } + /** Dispatches the current UI state to all registered subscriber callbacks. */ function notifyListeners() { diff --git a/src/layers/cctv/rendering.js b/src/layers/cctv/rendering.js index d10368d40..4b6f215e4 100644 --- a/src/layers/cctv/rendering.js +++ b/src/layers/cctv/rendering.js @@ -1,5 +1,4 @@ import * as Cesium from 'cesium'; -import { horizonOccluder } from '../../data/iconOrientation.js'; import { isHeadingEstimated } from './headingConfidence.js'; import { ACTIVE_CAMERA_COLOR, @@ -11,8 +10,21 @@ import { ACTIVE_COVERAGE_EDGE, IDLE_COVERAGE_EDGE_MUTED, ACTIVE_COVERAGE_EDGE_DEPTHFAIL, + VIEW_CELL_PX, + VIEW_ICON_MARGIN, + VIEW_MIN_CELL_M, } from './policy.js'; +/** FNV-1a over a camera id: a stable, well-spread order for crowded cells. */ +function idHash(id) { + let hash = 0x811c9dc5; + for (let i = 0; i < id.length; i++) { + hash ^= id.charCodeAt(i); + hash = Math.imul(hash, 0x01000193); + } + return hash >>> 0; +} + export function createRendering({ state: layerState, services, @@ -138,24 +150,186 @@ export function createRendering({ * (Google-3D regime) nothing writes far-side depth, and the billboards are now * always-on-top (`disableDepthTestDistance: INFINITY` — the far-zoom submerge * fix), so without this pass London's cluster would shine through the planet - * from a US viewpoint. Pure math over ≤ catalog-size points; runs on - * camera.moveEnd + init only (event-driven — no steady-state work). + * from a US viewpoint. + * + * The same pass thins the view so a large catalog stays readable and cheap: + * icons well off screen hide, and each camera competes for a world-anchored + * cell sized to VIEW_CELL_PX at its distance (healthiest first, then a stable + * id order). The active camera shows whenever it is above the horizon + * and near the screen, even in a crowded cell. The shown records become + * `_viewRecords`, the working set for geometry, cards and the camera picker; + * the array keeps its identity while the shown set is unchanged. + * + * Runs on camera settle, throttled during motion, on resize, enable and + * activation changes — never per frame. + * @returns {boolean} Whether the shown set changed. */ function refreshHorizonCulling() { if ( + !layerState._enabled || !layerState._viewer || layerState._viewer.isDestroyed() || !layerState._records.length ) - return; - const occluder = horizonOccluder(layerState._viewer.camera); + return false; + const { camera, scene } = layerState._viewer; + const width = scene.canvas.clientWidth || scene.canvas.width || 0; + const height = scene.canvas.clientHeight || scene.canvas.height || 0; + const minX = -width * VIEW_ICON_MARGIN; + const maxX = width * (1 + VIEW_ICON_MARGIN); + const minY = -height * VIEW_ICON_MARGIN; + const maxY = height * (1 + VIEW_ICON_MARGIN); + // Ground metres one pixel covers per metre of distance from the viewer. + const fovy = Number.isFinite(camera.frustum?.fovy) + ? camera.frustum.fovy + : Math.PI / 3; + const metresPerPixelPerMetre = + (2 * Math.tan(fovy / 2)) / Math.max(1, height); + const viewer = camera.positionWC; + // The horizon test of Cesium's EllipsoidalOccluder, inlined: this loop + // visits every camera in the catalog. + const inverseRadii = Cesium.Ellipsoid.WGS84.oneOverRadii; + const cx = viewer.x * inverseRadii.x; + const cy = viewer.y * inverseRadii.y; + const cz = viewer.z * inverseRadii.z; + const limbSquared = cx * cx + cy * cy + cz * cz - 1; + const activeId = layerState._activeCameraId; + const project = screenProjector(scene, camera, width, height); + const pass = (layerState._viewPassId = (layerState._viewPassId || 0) + 1); + const shown = []; + const cells = new Map(); + for (const record of layerState._records) { + const position = record.billboard?.position; + if (!position) continue; + const vx = position.x * inverseRadii.x - cx; + const vy = position.y * inverseRadii.y - cy; + const vz = position.z * inverseRadii.z - cz; + const vtDotVc = -(vx * cx + vy * cy + vz * cz); + if ( + limbSquared < 0 + ? vtDotVc > 0 + : vtDotVc > limbSquared && + (vtDotVc * vtDotVc) / (vx * vx + vy * vy + vz * vz) > limbSquared + ) + continue; + const screen = project(position); + if ( + !screen || + !(screen.x >= minX && screen.x <= maxX) || + !(screen.y >= minY && screen.y <= maxY) + ) + continue; + const dx = position.x - viewer.x; + const dy = position.y - viewer.y; + const dz = position.z - viewer.z; + const cellM = + VIEW_CELL_PX * + metresPerPixelPerMetre * + Math.sqrt(dx * dx + dy * dy + dz * dz); + if (record.camera.id === activeId || !(cellM >= VIEW_MIN_CELL_M)) { + record.viewPass = pass; + shown.push(record); + continue; + } + const key = worldCellKey(record, cellM); + const rank = viewRank(record); + const holder = cells.get(key); + if (!holder || rank < holder.rank) cells.set(key, { record, rank }); + } + for (const { record } of cells.values()) { + record.viewPass = pass; + shown.push(record); + } for (const record of layerState._records) { const bb = record.billboard; if (!bb) continue; - const visible = occluder.isPointVisible(bb.position); + const visible = record.viewPass === pass; if (bb.show !== visible) bb.show = visible; } + const previous = layerState._viewRecords; + const changed = + previous.length !== shown.length || + previous.some((record) => record.viewPass !== pass); + if (changed) layerState._viewRecords = shown; + return changed; + } + + /** + * Canvas coordinates for world positions. In 3D the pass multiplies by one + * view-projection matrix per call instead of Cesium's per-point + * cartesianToCanvasCoordinates, which dominated the pass on a large + * catalog; other scene modes keep Cesium's projection. + * @returns {(position: Cesium.Cartesian3) => ({x: number, y: number}|null)} + */ + function screenProjector(scene, camera, width, height) { + const out = layerState._scratchViewScreen; + const projection = camera.frustum?.projectionMatrix; + if ( + scene.mode !== Cesium.SceneMode.SCENE3D || + !projection || + !camera.viewMatrix + ) + return (position) => scene.cartesianToCanvasCoordinates(position, out); + const m = Cesium.Matrix4.multiply( + projection, + camera.viewMatrix, + layerState._scratchViewProjection, + ); + return (p) => { + const w = m[3] * p.x + m[7] * p.y + m[11] * p.z + m[15]; + const z = m[2] * p.x + m[6] * p.y + m[10] * p.z + m[14]; + // Behind the eye or before the near plane: Cesium does not draw it. + if (!(w > 0) || z < 0) return null; + out.x = + ((m[0] * p.x + m[4] * p.y + m[8] * p.z + m[12]) / w + 1) * 0.5 * width; + out.y = + (1 - (m[1] * p.x + m[5] * p.y + m[9] * p.z + m[13]) / w) * 0.5 * height; + return out; + }; + } + + /** + * A grid cell fixed to the ground: `cellM` rounded up to a power of two + * metres, so the cell (and its winner) only changes when the camera's + * distance crosses a doubling: a level pan keeps it, a tilted pan or a zoom + * can move cameras near the edge of a step to the next one. + */ + function worldCellKey(record, cellM) { + const level = Math.ceil(Math.log2(cellM)); + const { lat, lon } = record.camera; + // A camera's key changes only with the level (or an edited position), so + // it is kept per level. + if ( + record.viewCellLevel === level && + record.viewCellLat === lat && + record.viewCellLon === lon + ) + return record.viewCellKey; + const latStep = 2 ** level / 111_320; + const row = Math.floor(lat / latStep); + const rowLat = Cesium.Math.toRadians((row + 0.5) * latStep); + const lonStep = latStep / Math.max(0.05, Math.cos(rowLat)); + record.viewCellLevel = level; + record.viewCellLat = lat; + record.viewCellLon = lon; + record.viewCellKey = `${level}:${row}:${Math.floor(lon / lonStep)}`; + return record.viewCellKey; + } + + /** Lower ranks win a crowded cell: healthy feeds first, then a stable id order. */ + function viewRank(record) { + const status = layerState._healthById.get(record.camera.id)?.status; + const tier = + status === 'ok' + ? 0 + : status === 'degraded' + ? 1 + : status === 'offline' + ? 3 + : 2; + record.viewHash ??= idHash(record.camera.id); + return tier * 2 ** 32 + record.viewHash; } /** @@ -204,6 +378,8 @@ export function createRendering({ const activeId = activeRecord?.camera.id || null; const coverageVisible = parts.geometry.buildCoverageVisibleSet(activeRecord); + // Drawn coverage needs geometry too (geometryQueue's working set). + layerState._coverageVisibleIds = coverageVisible; const coverageOn = layerState._coverageMode !== 'off'; const viewshedOn = layerState._coverageMode === 'viewshed'; if (coverageOn) { @@ -215,8 +391,13 @@ export function createRendering({ for (const record of layerState._records) { const isActive = record.camera.id === activeId; if (record.billboard) { - record.billboard.color = cameraColorFor(record); - record.billboard.scale = isActive ? 1.25 : 1.0; + // Unchanged writes still dirty the collection, which then rewrites + // every icon's vertices: across a large catalog, write only changes. + const color = cameraColorFor(record); + if (!Cesium.Color.equals(record.billboard.color, color)) + record.billboard.color = color; + const scale = isActive ? 1.25 : 1.0; + if (record.billboard.scale !== scale) record.billboard.scale = scale; // disableDepthTestDistance stays POSITIVE_INFINITY for every billboard // (set at creation) — see the field-test far-zoom submerge fix there. } diff --git a/src/layers/cctv/selection.js b/src/layers/cctv/selection.js index 317d80c7e..c86425fd0 100644 --- a/src/layers/cctv/selection.js +++ b/src/layers/cctv/selection.js @@ -141,6 +141,11 @@ export function createSelection({ void parts.ground.resolveFootprintGround(record); record.activationDone = true; parts.rendering.refreshCoverageStyles(); + // The active camera's icon always shows; the previously active one + // competes for its cell again. Its coverage neighbours join the working + // set and are queued for grounding. + parts.rendering.refreshHorizonCulling(); + parts.geometryQueue.enqueueViewGeometry(); // The newly active camera leaves the ambient ring (its monitor plane takes // over); the freed slot re-fills on this same pass. parts.cards.refreshAmbientCards(); @@ -171,7 +176,9 @@ export function createSelection({ parts.ground.groundAltFor(previous), ); }); + parts.rendering.refreshHorizonCulling(); parts.rendering.refreshCoverageStyles(); + parts.geometryQueue.enqueueViewGeometry(); parts.cards.refreshAmbientCards(); layerState._gizmo?.refresh(); parts.presentation.notifyListeners(); diff --git a/src/layers/cctv/state.js b/src/layers/cctv/state.js index b78d18b73..78c2b8fa9 100644 --- a/src/layers/cctv/state.js +++ b/src/layers/cctv/state.js @@ -76,6 +76,63 @@ export function createState({ services }) { layerState._scratchFocusScreen = new Cesium.Cartesian2(); + layerState._scratchViewScreen = new Cesium.Cartesian2(); + + layerState._scratchViewProjection = new Cesium.Matrix4(); + + // Counts view passes; a record shown by the latest pass carries its number. + + layerState._viewPassId = 0; + + // Records whose icons the last view pass showed (see refreshHorizonCulling): + // geometry refinement, the tiles-ready completion pass and ambient cards + // work over these, not the whole catalog. + + layerState._viewRecords = []; + + // Coverage ids refreshCoverageStyles last drew: part of the geometry + // working set even when their icons are thinned out. + + layerState._coverageVisibleIds = new Set(); + + // View pass triggers besides camera settle: an interval while the camera + // moves, and a canvas resize observer. + + layerState._viewMotionTimer = 0; + + layerState._viewResizeObserver = null; + + layerState._viewResizeCanvas = null; + + // The camera the moveStart/moveEnd listeners are attached to. + + layerState._viewListenerCamera = null; + + // A motion pass changed the shown set and the settle pass has not yet told + // subscribers; the camera pose the last view pass ran at. + + layerState._viewUnpublished = false; + + layerState._viewPose = null; + + // { at, ms } — when the last motion pass ran and what it cost. + + layerState._viewMotionPass = null; + + // Retries the view geometry a drain left unresolved, once cells warm. + + layerState._viewGeometryRetryTimer = 0; + + // { key, count } — the active camera's coverage-overlap count, which the + // summary reads on every notification. + + layerState._neighborCountCache = null; + + // { view, active, list } — the camera picker's index uiState publishes, + // rebuilt only when the view pass or the active camera changes. + + layerState._cameraIndexCache = null; + // Staggered geometry-load queue state (see startGeometryLoadQueue). layerState._geoQueue = []; diff --git a/src/layers/cctv/testing.js b/src/layers/cctv/testing.js index 5c1e67956..c4aada9bb 100644 --- a/src/layers/cctv/testing.js +++ b/src/layers/cctv/testing.js @@ -79,6 +79,7 @@ export function createTesting({ state: layerState, services, parts, source }) { * @param {boolean} [options.enabled=true] Layer enabled state. * @param {'off'|'on'|'viewshed'} [options.coverageMode='on'] Coverage mode. * @param {boolean} [options.showProjection=false] Projection visibility. + * @param {Map} [options.healthById] Health rows by camera id. * @returns {void} */ @@ -89,6 +90,7 @@ export function createTesting({ state: layerState, services, parts, source }) { enabled = true, coverageMode = 'on', showProjection = false, + healthById = new Map(), } = {}) { layerState._viewer = viewer; layerState._records = Array.isArray(records) ? records : []; @@ -108,6 +110,11 @@ export function createTesting({ state: layerState, services, parts, source }) { 'on', ); layerState._showProjection = !!showProjection; + layerState._healthById = healthById; + layerState._viewRecords = []; + layerState._cameraIndexCache = null; + layerState._coverageVisibleIds = new Set(); + layerState._neighborCountCache = null; } return { _pushAmbientCardEntriesForTest, diff --git a/src/layers/directions/index.js b/src/layers/directions/index.js index 77d3118e9..ffc1497f1 100644 --- a/src/layers/directions/index.js +++ b/src/layers/directions/index.js @@ -18,9 +18,19 @@ import * as Cesium from 'cesium'; import { roadLabel } from '../../data/routeSteps.js'; -import { t, msg, formatNumber, translateKnown } from '../../i18n/index.js'; +import { + t, + msg, + formatNumber, + getLocale, + translateKnown, +} from '../../i18n/index.js'; import { pointerOwnerName } from '../../data/pointerOwnerNames.js'; -import { formatQuantity } from '../../ui/unitLabels.js'; +import { + formatQuantity, + quantitySeparator, + unitLabel, +} from '../../ui/unitLabels.js'; export const DIRECTIONS_STEP_OVERLAY_SOURCE_ID = 'directions-step'; export const DIRECTIONS_STEP_OVERLAY_SOURCE_OPTIONS = Object.freeze({ @@ -125,7 +135,9 @@ export function routeDurationText(seconds) { if (minutes < 60) return formatQuantity(minutes, 'min'); const hours = formatQuantity(Math.floor(minutes / 60), 'h'); const rest = minutes % 60; - return rest ? `${hours}\u00a0${formatQuantity(rest, 'min')}` : hours; + if (!rest) return hours; + // "1 h 5 min"; Japanese runs the parts together: 1時間5分. + return `${hours}${quantitySeparator(unitLabel('h'))}${formatQuantity(rest, 'min')}`; } /** "left", "slightly right", … as it reads inside a maneuver sentence. */ @@ -394,6 +406,16 @@ export function normalizeDirectionsParams(params = {}) { return out; } +/** + * A travel-mode word inside a sentence: lower case, except in German, where + * the words are nouns and keep their capital ("Auto", "Fahrrad"). + * @param {string} word Translated mode word. + * @returns {string} + */ +function modeWordInSentence(word) { + return getLocale() === 'de' ? word : word.toLowerCase(); +} + /** * Row chips for a given state. Pure so the chip logic is testable without a DOM. * @param {{mode:string, armed:null|'a'|'b', a:object|null, b:object|null, status:string, route:object|null}} state @@ -409,7 +431,7 @@ export function directionsRowControls(state) { state: mode === id ? 'active' : 'idle', title: t('{mode} — reroute for {modeLower}', { mode: t(spec.word), - modeLower: t(spec.word).toLowerCase(), + modeLower: modeWordInSentence(t(spec.word)), }), params: { mode: id }, })); diff --git a/src/overlays/worldOverlay.js b/src/overlays/worldOverlay.js index ccf3cbd59..1fcfdd8dc 100644 --- a/src/overlays/worldOverlay.js +++ b/src/overlays/worldOverlay.js @@ -114,7 +114,6 @@ export const WORLD_OVERLAY_OCCLUDER_SELECTORS = Object.freeze([ '.hud-top-right', '.hud-bottom-left', '.hud-bottom-right', - '.hud-top-bar', '.hud-bottom-bar', '#space-mission-panel', '#space-mission-panel-host', diff --git a/src/panelEscape.test.mjs b/src/panelEscape.test.mjs index d3d5cfc6d..b906ecf67 100644 --- a/src/panelEscape.test.mjs +++ b/src/panelEscape.test.mjs @@ -166,11 +166,6 @@ test('Cockpit utility Escape leaves an expanded nested Parameters panel to the s /const kind = displayOpen \? 'display' : 'radio';[\s\S]*?escapedFromDisclosure[\s\S]*?returnFocus: !escapedFromDisclosure[\s\S]*?disclosure\?\.blur/, 'Cockpit utility disclosures clear their own focus when Escape closes them', ); - assert.match( - radioBindings, - /_contextRadioDock\?\.classList\.contains\('disclosure-open'\)[\s\S]*?escapedFromDisclosure[\s\S]*?setRadioDisclosure\(false, \{ returnFocus: !escapedFromDisclosure \}\)[\s\S]*?_contextRadioToggleBtn\?\.blur/, - 'compact Radio disclosure clears its own focus when Escape closes it', - ); }); diff --git a/src/radioMarkup.test.mjs b/src/radioMarkup.test.mjs index 9d9ae45ed..1774b1f43 100644 --- a/src/radioMarkup.test.mjs +++ b/src/radioMarkup.test.mjs @@ -206,10 +206,11 @@ test('no unchanged Realtime tool definition drifts silently', () => { .sort((a, b) => a.name.localeCompare(b.name)) .map((tool) => structuredClone(tool)) .filter((tool) => tool.name !== 'set_cyber_sonar'); - // Cyber adds one HUD choice and the sonar tool; retain the existing pin for every legacy field. + // Cyber adds one HUD choice and the sonar tool, and Operator was retired; + // retain the existing pin for every legacy field. const hudLayout = unchanged.find((tool) => tool.name === 'set_hud').parameters.properties.layout; - assert.deepEqual(hudLayout.enum, ['tactical', 'operator', 'minimal', 'cyber']); - hudLayout.enum = hudLayout.enum.filter((layout) => layout !== 'cyber'); + assert.deepEqual(hudLayout.enum, ['tactical', 'minimal', 'cyber']); + hudLayout.enum = ['tactical', 'operator', 'minimal']; // Globe rotation adds one motion and clarifies the two affected descriptions. // Check those edits explicitly, then keep the legacy pin for every other field. const camera = unchanged.find((tool) => tool.name === 'move_camera'); @@ -262,7 +263,7 @@ test('no unchanged Realtime tool definition drifts silently', () => { }); test('Radio volume and mission speed share the Sharpen slider visual language', () => { - for (const id of ['cockpit-radio-volume', 'context-radio-mini-volume', 'radio-volume', 'sdr-volume']) { + for (const id of ['cockpit-radio-volume', 'radio-volume', 'sdr-volume']) { assert.match( html, new RegExp(`id="${id}"[^>]*class="gev-quantitative-slider"[^>]*type="range"`), @@ -282,22 +283,17 @@ test('Radio volume and mission speed share the Sharpen slider visual language', assert.doesNotMatch(css, /#space-mission-panel \[data-mission-replay-speed\]::-webkit-slider-thumb/); }); -test('Radio is nested inside Context with separate disclosure and power controls', () => { +test('Radio is nested inside Context with its own disclosure and power controls', () => { const contextStart = html.indexOf('id="global-context-panel"'); const radioStart = html.indexOf('id="radio-panel"'); const contextEnd = html.indexOf('\n ', contextStart); assert.ok(contextStart >= 0 && radioStart > contextStart && radioStart < contextEnd); assert.match(html, /id="radio-panel"[^>]*data-panel-id="radio-panel"/); assert.match(html, /aria-label="Radio playback"/); - assert.match(html, /id="context-radio-toggle-btn"[^>]*aria-expanded="false"[^>]*aria-controls="context-radio-mini"/); - assert.doesNotMatch(html, /id="context-radio-toggle-btn"[^>]*aria-pressed=/); - assert.match(html, /id="context-radio-mini"[^>]*aria-label="Compact Radio controls"[^>]*hidden/); - assert.match(html, /id="context-radio-mini-enable-btn"[^>]*aria-pressed="false"/); - assert.match(html, /id="context-radio-details-btn"[^>]*aria-expanded="false"[^>]*aria-controls="radio-panel"/); - assert.match(html, /id="context-radio-details-btn"[\s\S]*?
-
diff --git a/src/ui/templates/display-controls.html b/src/ui/templates/display-controls.html index f6af31127..458a2fcd8 100644 --- a/src/ui/templates/display-controls.html +++ b/src/ui/templates/display-controls.html @@ -15,7 +15,6 @@ Layout diff --git a/src/ui/unitLabels.js b/src/ui/unitLabels.js index 1d9bd6a61..3b12da006 100644 --- a/src/ui/unitLabels.js +++ b/src/ui/unitLabels.js @@ -1,4 +1,4 @@ -import { formatNumber, msg, t } from '../i18n/index.js'; +import { formatNumber, getLocale, msg, t } from '../i18n/index.js'; // Unit ids stay English in data, params and AI-tool readings; only the label // shown on the page is translated. dBZ and °C read the same in every language. @@ -23,6 +23,17 @@ export function unitLabel(unit) { return Object.hasOwn(UNIT_LABELS, unit) ? t(UNIT_LABELS[unit]) : unit || ''; } +/** + * What goes between a number and its unit label: a no-break space, except + * before a Japanese unit word, which follows the number directly (5分, 1時間). + */ +export function quantitySeparator(label) { + return getLocale() === 'ja' && + /^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u.test(label) + ? '' + : '\u00a0'; +} + /** * A number and its unit that never break apart: "22,0 км/ч". Readings carry * no digit grouping ("1008 hPa"), as on weather charts. @@ -30,7 +41,7 @@ export function unitLabel(unit) { export function formatQuantity(value, unit, options) { const label = unitLabel(unit); const number = formatNumber(value, { useGrouping: false, ...options }); - return label ? `${number}\u00a0${label}` : number; + return label ? `${number}${quantitySeparator(label)}${label}` : number; } /** diff --git a/src/ui/unitLabels.test.mjs b/src/ui/unitLabels.test.mjs index c0b1932b1..14b6030e3 100644 --- a/src/ui/unitLabels.test.mjs +++ b/src/ui/unitLabels.test.mjs @@ -44,3 +44,14 @@ test('other languages get their decimal separator and no digit grouping', () => setLocale('en'); } }); + +test('Japanese unit words follow the number directly; symbols keep the space', () => { + try { + setLocale('ja'); + assert.equal(formatQuantity(5, 'min'), '5分'); + assert.equal(formatQuantity(1, 'h'), '1時間'); + assert.equal(formatQuantity(95, 'kt'), '95\u00a0kt'); + } finally { + setLocale('en'); + } +}); diff --git a/src/voice/actionSchemas.test.mjs b/src/voice/actionSchemas.test.mjs index 3bf9cfcd9..4e6586729 100644 --- a/src/voice/actionSchemas.test.mjs +++ b/src/voice/actionSchemas.test.mjs @@ -46,7 +46,8 @@ test('the complete Realtime tool payload pins the manifest-generated layer relea // Panel adds named voice targets and timeout; shared Panel/Cockpit guidance // is consolidated without changing either feature's arguments. // Snow leaves the set_visual_style enum. - '724a82b538a12a994401fc941e175625b032bfb96109934e091d3ef5b9600909', + // The Operator HUD layout leaves set_hud's layout enum. + 'f47a7f78f13b1f79978fa524fad2618e32bd813e1a5dfd746bf35d29909fc91b', ); }); @@ -167,9 +168,10 @@ test('all legacy action arguments are byte-identical after removing the delibera delete center.properties[axis].minimum; delete center.properties[axis].maximum; } - // Cyber adds one HUD layout. + // Cyber adds one HUD layout and Operator was retired. const hud = property('set_hud').layout; - hud.enum = hud.enum.filter((layout) => layout !== 'cyber'); + assert.deepEqual(hud.enum, ['tactical', 'minimal', 'cyber']); + hud.enum = ['tactical', 'operator', 'minimal']; // Snow is retired from set_visual_style; restore it to keep the legacy pin. const visualStyle = legacy.find((tool) => tool.name === 'set_visual_style') .parameters.properties.style; diff --git a/src/voice/gevActions.js b/src/voice/gevActions.js index 0fd17dc77..299fbfc61 100644 --- a/src/voice/gevActions.js +++ b/src/voice/gevActions.js @@ -1659,9 +1659,8 @@ export async function controlCctv(dataManager, args = {}, styleManager = null) { return { activeCameraId: ui.activeCameraId || null, activeCamera: ui.activeCamera?.name || ui.activeCamera?.id || null, - cameraCount: Array.isArray(ui.cameras) - ? ui.cameras.length - : ui.count || 0, + // `count`, not `cameras.length`: reading `cameras` builds every camera. + cameraCount: Number.isFinite(ui.count) ? ui.count : 0, showCoverage: !!ui.showCoverage, coverageMode: ui.coverageMode || (ui.showCoverage ? 'on' : 'off'), showProjection: !!ui.showProjection, diff --git a/src/voice/speech.js b/src/voice/speech.js index c53881595..cbfae621f 100644 --- a/src/voice/speech.js +++ b/src/voice/speech.js @@ -963,7 +963,10 @@ export function progressStepLabel(step, label) { : step ? spokenLabel(step, 24) : t('Working'); - return label ? `${base}: ${spokenLabel(label, 32)}` : base; + // The join is a catalog string so each language spaces its colon. + return label + ? t('{name}: {state}', { name: base, state: spokenLabel(label, 32) }) + : base; } /** diff --git a/src/voice/speech.test.mjs b/src/voice/speech.test.mjs index 36482290d..cc11fef6e 100644 --- a/src/voice/speech.test.mjs +++ b/src/voice/speech.test.mjs @@ -9,6 +9,7 @@ import { spokenLabel, } from './speech.js'; import { countWords, findHedges } from './speechLint.js'; +import { setLocale } from '../i18n/index.js'; function envelope(name, result, args) { return attachVoiceResult(name, result, args); @@ -459,3 +460,15 @@ test('labels are bounded and cleaned before they are spoken', () => { 'background steps stay on screen', ); }); + +test('progress step labels join through the catalog colon', () => { + setLocale('fr'); + try { + assert.equal( + progressStepLabel('resolve', 'Capitol'), + 'Recherche de lieux\u202f: Capitol', + ); + } finally { + setLocale('en'); + } +});