From 37acb4842f65c6725146216367eb54b31cf2cadc Mon Sep 17 00:00:00 2001 From: Said Date: Mon, 19 Oct 2026 23:23:52 +0100 Subject: [PATCH 01/25] fix(docker): honor trusted-peer environment overrides --- README.md | 10 ++ SECURITY.md | 5 + compose.yaml | 5 + docs/CURRENT-STATE.md | 9 ++ scripts/docker-start.mjs | 12 ++- scripts/format-scope.json | 3 +- src/dockerLauncher.test.mjs | 3 +- src/dockerTrustedPeers.test.mjs | 179 ++++++++++++++++++++++++++++++++ 8 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 src/dockerTrustedPeers.test.mjs diff --git a/README.md b/README.md index 3b005c8dc..eb00b9635 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,16 @@ Publishing it wider is the [Sharing an instance](#-sharing-an-instance) opt-in, When you widen the publish, also set `GEV_KEY_SETUP_TRUSTED_PEERS=none` so the launcher trusts no gateway address. +```bash +GEV_KEY_SETUP_TRUSTED_PEERS=none docker compose up +``` + +Compose forwards `GEV_KEY_SETUP_TRUSTED_PEERS` and `GEV_ALLOWED_HOSTS` from your +shell or the project's `.env`; an exported shell value takes precedence. You can +also put `GEV_KEY_SETUP_TRUSTED_PEERS=none` in `.env` for a persistent setting. +Unset or blank trusted peers retain automatic gateway discovery; `none` explicitly +disables it. The startup log confirms that no additional peers are trusted. +
Running on Linux, a NAS, or behind a reverse proxy diff --git a/SECURITY.md b/SECURITY.md index b694d8dcd..aa2f5a397 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -98,6 +98,11 @@ The dev server is a **key broker**: every server-side key above is spendable by `compose.yaml` publishes on `127.0.0.1`: **widen that mapping and every LAN client arrives from the same gateway address, which hands them Provider Settings too.** Treat publishing beyond loopback as the LAN opt-in above. + Compose forwards `GEV_KEY_SETUP_TRUSTED_PEERS` and `GEV_ALLOWED_HOSTS` from + the shell or project `.env` before the launcher runs. Set trusted peers to + `none` when widening the publish: it becomes an explicit empty list before + Vite loads configuration, so the gateway cannot regain Provider Settings + access. Unset or blank values preserve automatic gateway discovery. - **Provider-side budgets are the real backstop.** For hard spend protection, configure limits where the money is: OpenAI platform usage limits, Google Cloud budget alerts + per-API quotas, and equivalent controls for any other keyed provider. - **Pinokio LAN and Cloudflare sharing are refused.** The current supported Pinokio release re-reads sharing state when an app registers its Open URL and diff --git a/compose.yaml b/compose.yaml index 7826bf376..c2d3bf3d4 100644 --- a/compose.yaml +++ b/compose.yaml @@ -5,6 +5,11 @@ services: # lands in the bind-mounted repo, so run as you, not root. UID/GID are # unset on Docker Desktop, where the 1000 fallback is ignored anyway. user: "${UID:-1000}:${GID:-1000}" + # Forward explicit shell/.env policy before the launcher discovers a gateway. + # Unset values stay unset so the default loopback-only install still works. + environment: + - GEV_KEY_SETUP_TRUSTED_PEERS + - GEV_ALLOWED_HOSTS ports: # Publish to loopback only. Docker's default ("4173:4173") would bind # every host interface, which quietly turns the LAN warning in the README diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 60937dd05..300ee9279 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1,5 +1,14 @@ # God's Eye View Current State +Docker Compose forwards `GEV_KEY_SETUP_TRUSTED_PEERS` and `GEV_ALLOWED_HOSTS` +from the shell or project `.env` before `scripts/docker-start.mjs` runs; shell +values take precedence. The launcher's `none` policy resolves to an empty trusted +peer list and is assigned before importing Vite, preventing later dotenv loading +from replacing it. Unset or blank values retain automatic gateway discovery. +Startup logs distinguish an explicit `none` policy from a missing default route. +This changes neither loopback-only port publishing nor the Provider Settings +Host, Origin, proxy-header and sharing checks. + Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is canonical; `greatCircleMeters` is its exact metre conversion. Missing, diff --git a/scripts/docker-start.mjs b/scripts/docker-start.mjs index 3935f2b3b..c77d6335f 100644 --- a/scripts/docker-start.mjs +++ b/scripts/docker-start.mjs @@ -48,7 +48,8 @@ export function defaultGatewayFromRouteTable(text) { /** * Decide this launch's trusted-peer list. An operator value always wins, so a - * compose override stays authoritative; otherwise the gateway alone is + * compose override stays authoritative; `none` trusts no additional peers. + * An unset or blank value means the gateway alone is * trusted. No gateway trusts nothing and Provider Settings stays loopback-only, * exactly as under `npm run dev`. * @param {{env?: NodeJS.ProcessEnv, routeTable?: string}} input @@ -56,6 +57,7 @@ export function defaultGatewayFromRouteTable(text) { */ export function resolveTrustedPeers({ env = process.env, routeTable = '' } = {}) { const explicit = String(env.GEV_KEY_SETUP_TRUSTED_PEERS ?? '').trim(); + if (explicit.toLowerCase() === 'none') return ''; if (explicit) return explicit; return defaultGatewayFromRouteTable(routeTable) ?? ''; } @@ -109,15 +111,17 @@ async function start() { String(process.env.GEV_KEY_SETUP_TRUSTED_PEERS ?? '').trim(), ); const trustedPeers = resolveTrustedPeers({ routeTable: readRouteTable() }); + // Set even an empty list before Vite loads .env: the resolved launcher policy + // must remain authoritative when there is no trusted gateway. + process.env.GEV_KEY_SETUP_TRUSTED_PEERS = trustedPeers; if (trustedPeers) { - // Set before Vite is imported: the dev server snapshots process.env during - // configuration, and the admission gate reads it per request. - process.env.GEV_KEY_SETUP_TRUSTED_PEERS = trustedPeers; console.log( fromOperator ? `[Docker] Provider Settings trusted peers: ${trustedPeers} (GEV_KEY_SETUP_TRUSTED_PEERS).` : `[Docker] Provider Settings trusts the container gateway (${trustedPeers}).`, ); + } else if (fromOperator) { + console.log('[Docker] Provider Settings trusts no additional peers (GEV_KEY_SETUP_TRUSTED_PEERS=none).'); } else { console.log('[Docker] No default gateway; Provider Settings stays loopback-only.'); } diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 543b0f9a4..faac60584 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -1070,5 +1070,6 @@ "scripts/assemble-changelog.mjs", "src/tooling/changelogFragments.test.mjs", "src/data/gbfsProxyRateLimit.test.mjs", - "src/terrainHeightsBounds.test.mjs" + "src/terrainHeightsBounds.test.mjs", + "src/dockerTrustedPeers.test.mjs" ] diff --git a/src/dockerLauncher.test.mjs b/src/dockerLauncher.test.mjs index ae9987b01..213fe59dc 100644 --- a/src/dockerLauncher.test.mjs +++ b/src/dockerLauncher.test.mjs @@ -72,6 +72,7 @@ test('the Docker install path keeps its loopback-only, launcher-owned contract', const compose = readFileSync(new URL('../compose.yaml', import.meta.url), 'utf8'); assert.match(compose, /^\s+- "127\.0\.0\.1:4173:4173"$/m, 'the port is published on the host loopback only'); - assert.doesNotMatch(compose, /GEV_KEY_SETUP_TRUSTED_PEERS/, 'trust is derived at launch, never hard-coded'); + assert.match(compose, /^\s+- GEV_KEY_SETUP_TRUSTED_PEERS$/m, 'the operator policy reaches the launcher without a hard-coded address'); + assert.match(compose, /^\s+- GEV_ALLOWED_HOSTS$/m, 'the host allowlist reaches Vite'); assert.doesNotMatch(compose, /network_mode/, 'the container keeps its own network namespace'); }); diff --git a/src/dockerTrustedPeers.test.mjs b/src/dockerTrustedPeers.test.mjs new file mode 100644 index 000000000..74343bd04 --- /dev/null +++ b/src/dockerTrustedPeers.test.mjs @@ -0,0 +1,179 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import test from 'node:test'; +import { resolveTrustedPeers } from '../scripts/docker-start.mjs'; + +const ROUTES = 'Iface Destination Gateway Flags\neth0 00000000 0100A8C0 0003\n'; +const launcher = fileURLToPath( + new URL('../scripts/docker-start.mjs', import.meta.url), +); +const gateUrl = new URL('./keySetupCore.mjs', import.meta.url).href; + +test('none resolves to an empty trust list even with a valid container gateway', () => { + for (const value of ['none', ' NONE ', 'None']) { + assert.equal( + resolveTrustedPeers({ + env: { GEV_KEY_SETUP_TRUSTED_PEERS: value }, + routeTable: ROUTES, + }), + '', + ); + } +}); + +function launchWithPolicy(value, routeTable = ROUTES) { + // Run the real entry point and routing-table reader. Replace only the Linux + // route file and Vite's listening server so this works on macOS and CI too. + const fakeVite = ` + import { admitKeySetupRequest } from ${JSON.stringify(gateUrl)}; + const trustedAtImport = process.env.GEV_KEY_SETUP_TRUSTED_PEERS; + export async function createServer() { + const request = { + method: 'POST', hostHeader: 'localhost:4173', + origin: 'http://localhost:4173', contentType: 'application/json', + env: { GEV_KEY_SETUP_TRUSTED_PEERS: process.env.GEV_KEY_SETUP_TRUSTED_PEERS } + }; + console.log('POLICY ' + JSON.stringify({ + trustedAtImport, + allowedHosts: process.env.GEV_ALLOWED_HOSTS, + gateway: admitKeySetupRequest({ ...request, remoteAddress: '192.168.0.1' }), + loopback: admitKeySetupRequest({ ...request, remoteAddress: '127.0.0.1' }), + foreignOrigin: admitKeySetupRequest({ ...request, remoteAddress: '192.168.0.1', origin: 'https://foreign.example' }) + })); + return { async listen() {}, printUrls() {}, async close() {} }; + } + `; + const preload = ` + import fs from 'node:fs'; + import { registerHooks, syncBuiltinESMExports } from 'node:module'; + const originalRead = fs.readFileSync; + fs.readFileSync = (file, ...args) => file === '/proc/net/route' + ? ${JSON.stringify(routeTable)} : originalRead(file, ...args); + syncBuiltinESMExports(); + registerHooks({ resolve(specifier, context, next) { + if (specifier === 'vite') return { + url: ${JSON.stringify(`data:text/javascript,${encodeURIComponent(fakeVite)}`)}, shortCircuit: true + }; + return next(specifier, context); + }}); + `; + const env = { ...process.env, GEV_ALLOWED_HOSTS: 'globe.example.lan' }; + delete env.GEV_KEY_SETUP_TRUSTED_PEERS; + if (value !== undefined) env.GEV_KEY_SETUP_TRUSTED_PEERS = value; + const child = spawnSync( + process.execPath, + [ + '--import', + `data:text/javascript,${encodeURIComponent(preload)}`, + launcher, + ], + { env, encoding: 'utf8', timeout: 10000 }, + ); + assert.ifError(child.error); + assert.equal(child.status, 0, child.stderr); + const line = child.stdout + .split('\n') + .find((line) => line.startsWith('POLICY ')); + assert.ok(line, child.stdout); + return { ...JSON.parse(line.slice(7)), output: child.stdout }; +} + +test('the real Docker entry point seals none before importing Vite and the setup gate refuses the gateway', () => { + const result = launchWithPolicy('none'); + assert.equal(result.trustedAtImport, ''); + assert.equal(result.gateway.status, 403); + assert.equal(result.gateway.code, 'not-loopback'); + assert.equal(result.loopback.ok, true); + assert.equal(result.allowedHosts, 'globe.example.lan'); + assert.match(result.output, /trusts no additional peers/); + assert.doesNotMatch(result.output, /No default gateway/); +}); + +test('unset, blank and explicit peers retain gateway admission without weakening Origin checks', () => { + for (const value of [undefined, '', ' ', '192.168.0.1']) { + const result = launchWithPolicy(value); + assert.equal(result.trustedAtImport, '192.168.0.1'); + assert.equal(result.gateway.ok, true); + assert.equal(result.foreignOrigin.ok, false); + } + const result = launchWithPolicy(undefined, ''); + assert.equal(result.trustedAtImport, ''); + assert.equal(result.gateway.status, 403); + assert.match(result.output, /No default gateway/); +}); + +test('Compose resolves shell and project dotenv policy before the launcher starts', (t) => { + const version = spawnSync('docker', ['compose', 'version'], { + encoding: 'utf8', + timeout: 10000, + }); + if (version.error || version.status !== 0) + return t.skip('Docker Compose CLI is not installed'); + const root = mkdtempSync(path.join(tmpdir(), 'gev-compose-policy-')); + t.after(() => rmSync(root, { recursive: true, force: true })); + writeFileSync( + path.join(root, 'compose.yaml'), + readFileSync(new URL('../compose.yaml', import.meta.url)), + ); + const baseEnv = { ...process.env }; + for (const key of Object.keys(baseEnv)) { + if ( + key.startsWith('COMPOSE_') || + ['GEV_KEY_SETUP_TRUSTED_PEERS', 'GEV_ALLOWED_HOSTS'].includes(key) + ) + delete baseEnv[key]; + } + function config(dotenv = '', overrides = {}) { + writeFileSync(path.join(root, '.env'), dotenv); + const child = spawnSync( + 'docker', + [ + 'compose', + '-f', + 'compose.yaml', + '--env-file', + '.env', + 'config', + '--format', + 'json', + ], + { + cwd: root, + env: { ...baseEnv, ...overrides }, + encoding: 'utf8', + timeout: 10000, + }, + ); + assert.ifError(child.error); + assert.equal(child.status, 0, child.stderr); + return JSON.parse(child.stdout).services['gods-eye-view'].environment || {}; + } + const unset = config(); + assert.ok(unset.GEV_KEY_SETUP_TRUSTED_PEERS == null); + assert.ok(unset.GEV_ALLOWED_HOSTS == null); + const dotenv = + 'GEV_KEY_SETUP_TRUSTED_PEERS=none\nGEV_ALLOWED_HOSTS=globe.example.lan\n'; + assert.deepEqual(config(dotenv), { + GEV_KEY_SETUP_TRUSTED_PEERS: 'none', + GEV_ALLOWED_HOSTS: 'globe.example.lan', + }); + assert.equal( + config(dotenv, { GEV_KEY_SETUP_TRUSTED_PEERS: '10.9.8.7' }) + .GEV_KEY_SETUP_TRUSTED_PEERS, + '10.9.8.7', + ); + assert.equal( + config('', { GEV_KEY_SETUP_TRUSTED_PEERS: 'none' }) + .GEV_KEY_SETUP_TRUSTED_PEERS, + 'none', + ); + assert.equal( + config(dotenv, { GEV_KEY_SETUP_TRUSTED_PEERS: '' }) + .GEV_KEY_SETUP_TRUSTED_PEERS, + '', + ); +}); From f11fd3cb0525645431ede32980a65fbe55e4363a Mon Sep 17 00:00:00 2001 From: Said Date: Mon, 19 Oct 2026 23:49:59 +0100 Subject: [PATCH 02/25] docs: add changelog for Docker trusted-peer fix --- changelog.d/1265-docker-trusted-peers.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/1265-docker-trusted-peers.md diff --git a/changelog.d/1265-docker-trusted-peers.md b/changelog.d/1265-docker-trusted-peers.md new file mode 100644 index 000000000..f86fe3732 --- /dev/null +++ b/changelog.d/1265-docker-trusted-peers.md @@ -0,0 +1,3 @@ +- Honor Docker Compose trusted-peer and allowed-host settings from the shell + or project `.env`, and let `GEV_KEY_SETUP_TRUSTED_PEERS=none` disable gateway + access to Provider Settings before Vite starts (asturksever, #1265). From ed2f0685e0e011e355283cc64bdd1ecbc52114b5 Mon Sep 17 00:00:00 2001 From: Said Date: Tue, 20 Oct 2026 00:25:17 +0100 Subject: [PATCH 03/25] fix(aircraft): coalesce refreshes and cool down OpenSky failures --- docs/CURRENT-STATE.md | 7 + server/providers/aircraft/adsb-lol.js | 111 +++++++----- server/providers/aircraft/opensky.js | 252 ++++++++++++++------------ src/tooling/liveProviders.test.mjs | 121 +++++++++++++ 4 files changed, 333 insertions(+), 158 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 55b7a63ab..8ce0c5ff1 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1,5 +1,12 @@ # God's Eye View Current State +Global OpenSky and adsb.lol military snapshot refreshes are single-flight through +body reading and cache publication. Each caller retains its own response and +viewport-specific regional fallback. OpenSky 5xx responses cancel their bodies, +start a 15-second cooldown and serve dated last-good data with the upstream status +in the stale reason. Cold failures use the existing regional fallback or a JSON +upstream error; no error HTML is relayed as JSON. + Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is canonical; `greatCircleMeters` is its exact metre conversion. Missing, diff --git a/server/providers/aircraft/adsb-lol.js b/server/providers/aircraft/adsb-lol.js index 8fc40170d..b70da4f32 100644 --- a/server/providers/aircraft/adsb-lol.js +++ b/server/providers/aircraft/adsb-lol.js @@ -1,4 +1,7 @@ -import { readResponseTextCapped } from '../common/http.js'; +import { + coalesceProxyRequest, + readResponseTextCapped, +} from '../common/http.js'; /** * Vite plugin: adsb.lol military aircraft proxy with 12 s response cache. @@ -19,6 +22,7 @@ import { readResponseTextCapped } from '../common/http.js'; export function adsbLolProxy() { /** @type {string|null} Cached upstream JSON body. */ let _cache = null; + const inFlight = new Map(); /** @type {number} Epoch-ms when the cache was populated. */ let _cacheAt = 0; /** @type {number} Epoch-ms until which upstream is not contacted. */ @@ -104,60 +108,73 @@ export function adsbLolProxy() { ); return; } - const signal = AbortSignal.timeout(UPSTREAM_TIMEOUT_MS); - const upstream = await fetch('https://api.adsb.lol/v2/mil', { - headers: { 'User-Agent': 'gods-eye-view-adsblol-proxy/1.0' }, - signal, - }); - if (upstream.ok) { + const refresh = coalesceProxyRequest(inFlight, 'global', async () => { + const result = (status, body, cacheStatus, extra = {}) => ({ + status, + body, + cacheStatus, + extra, + }); + const signal = AbortSignal.timeout(UPSTREAM_TIMEOUT_MS); + const upstream = await fetch('https://api.adsb.lol/v2/mil', { + headers: { 'User-Agent': 'gods-eye-view-adsblol-proxy/1.0' }, + signal, + }); + if (upstream.ok) { + const body = await readResponseTextCapped( + upstream, + ADSBLOL_MIL_MAX_RESPONSE_BYTES, + signal, + ); + _cache = body; + _cacheAt = Date.now(); + _cooldownUntil = 0; + _cooldownStatus = 0; + return result(200, body, 'MISS'); + } + if (upstream.status === 429 || upstream.status >= 500) { + const failedAt = Date.now(); + _cooldownUntil = failedAt + cooldownFor(upstream, failedAt); + _cooldownStatus = upstream.status; + console.warn( + `[adsb.lol Proxy] upstream ${upstream.status}; cooling down ${Math.round((_cooldownUntil - Date.now()) / 1000)} s${_cache ? ', serving stale' : ''}`, + ); + if (_cache) { + // Do not wait for a failing response body before serving usable data. + upstream.body?.cancel().catch(() => {}); + return result(200, _cache, 'STALE', { + 'X-ADS-B-Upstream-Status': String(upstream.status), + 'X-ADS-B-Retry-After-Seconds': String( + Math.ceil((_cooldownUntil - Date.now()) / 1000), + ), + }); + } + } const body = await readResponseTextCapped( upstream, ADSBLOL_MIL_MAX_RESPONSE_BYTES, signal, ); - _cache = body; - _cacheAt = Date.now(); - _cooldownUntil = 0; - _cooldownStatus = 0; - serve(res, 200, body, 'MISS'); - return; - } - if (upstream.status === 429 || upstream.status >= 500) { - const failedAt = Date.now(); - _cooldownUntil = failedAt + cooldownFor(upstream, failedAt); - _cooldownStatus = upstream.status; - console.warn( - `[adsb.lol Proxy] upstream ${upstream.status}; cooling down ${Math.round((_cooldownUntil - Date.now()) / 1000)} s${_cache ? ', serving stale' : ''}`, + return result( + upstream.status, + body, + 'MISS', + _cooldownUntil > Date.now() + ? { + 'Retry-After': String( + Math.ceil((_cooldownUntil - Date.now()) / 1000), + ), + } + : {}, ); - if (_cache) { - // Do not wait for a failing response body before serving usable data. - upstream.body?.cancel().catch(() => {}); - serve(res, 200, _cache, 'STALE', { - 'X-ADS-B-Upstream-Status': String(upstream.status), - 'X-ADS-B-Retry-After-Seconds': String( - Math.ceil((_cooldownUntil - Date.now()) / 1000), - ), - }); - return; - } - } - const body = await readResponseTextCapped( - upstream, - ADSBLOL_MIL_MAX_RESPONSE_BYTES, - signal, - ); + }); + const response = await refresh.promise; serve( res, - upstream.status, - body, - 'MISS', - _cooldownUntil > Date.now() - ? { - 'Retry-After': String( - Math.ceil((_cooldownUntil - Date.now()) / 1000), - ), - } - : {}, + response.status, + response.body, + response.cacheStatus, + response.extra, ); } catch (e) { console.error('[adsb.lol Proxy]', e.message); diff --git a/server/providers/aircraft/opensky.js b/server/providers/aircraft/opensky.js index 752b3cf64..a69885e2f 100644 --- a/server/providers/aircraft/opensky.js +++ b/server/providers/aircraft/opensky.js @@ -67,15 +67,24 @@ async function fetchOpenSkyStates(headers) { try { const signal = AbortSignal.timeout(OPENSKY_ATTEMPT_TIMEOUT_MS); const upstream = await fetch(OPENSKY_STATES_URL, { headers, signal }); - if (upstream.status === 429) { + if (upstream.status === 429 || upstream.status >= 500) { // Start the cooldown from the headers and drop the rejected body // unread, so a stalled or oversized 429 body cannot skip it. _openskyCooldownUntil = - Date.now() + openSkyRateLimitCooldownMs(upstream); + Date.now() + + (upstream.status === 429 + ? openSkyRateLimitCooldownMs(upstream) + : 15000); + _openskyCooldownStatus = upstream.status; void upstream.body?.cancel().catch(() => {}); return { upstream, - body: JSON.stringify({ error: 'OpenSky rate limited.' }), + body: JSON.stringify({ + error: + upstream.status === 429 + ? 'OpenSky rate limited.' + : 'OpenSky upstream unavailable.', + }), }; } const body = await readResponseTextCapped( @@ -109,6 +118,8 @@ async function fetchOpenSkyStates(headers) { let _openskyTtlMs = OPENSKY_CACHE_MS; /** @type {number} Epoch-ms before which no upstream fetch is attempted. */ let _openskyCooldownUntil = 0; +let _openskyCooldownStatus = 429; +const _openskySnapshotInFlight = new Map(); /** * Picks the cache TTL from the remaining daily credit budget. * Client polls every 30 s, so tiers ≤30 s cost the same 480 credits/h; the @@ -460,7 +471,9 @@ export function openSkyProxy() { requestedMode: cachedMeta.requestedMode || requestedMode, usedMode: cachedMeta.usedMode || 'unknown', reason: isStale - ? 'rate_limited_serving_stale' + ? _openskyCooldownStatus === 429 + ? 'rate_limited_serving_stale' + : `upstream_http_${_openskyCooldownStatus}_serving_stale` : cachedMeta.reason || 'cached', staleSeconds: isStale ? (now - _openskyCacheTime) / 1000 @@ -487,80 +500,140 @@ export function openSkyProxy() { ) return; res.writeHead( - 429, + _openskyCooldownStatus, buildOpenSkyHeaders({ cacheStatus: 'COOLDOWN', requestedMode, usedMode: 'none', - reason: 'rate_limited', + reason: + _openskyCooldownStatus === 429 + ? 'rate_limited' + : `upstream_http_${_openskyCooldownStatus}`, retryAfterSeconds: (_openskyCooldownUntil - now) / 1000, }), ); res.end( JSON.stringify({ - error: 'OpenSky rate limited; proxy cooling down.', + error: 'OpenSky upstream unavailable; proxy cooling down.', }), ); return; } - const basicUser = process.env.OPENSKY_USERNAME || ''; - const basicPass = process.env.OPENSKY_PASSWORD || ''; - const hasBasicCreds = Boolean(basicUser && basicPass); - const headers = { Accept: 'application/json' }; - let usedMode = 'anon'; - let reason = 'forced_anonymous'; + const refresh = coalesceProxyRequest( + _openskySnapshotInFlight, + 'global', + async () => { + const basicUser = process.env.OPENSKY_USERNAME || ''; + const basicPass = process.env.OPENSKY_PASSWORD || ''; + const hasBasicCreds = Boolean(basicUser && basicPass); + const headers = { Accept: 'application/json' }; + let usedMode = 'anon'; + let reason = 'forced_anonymous'; - if (requestedMode === 'basic') { - if (hasBasicCreds) { - headers.Authorization = `Basic ${Buffer.from(`${basicUser}:${basicPass}`).toString('base64')}`; - usedMode = 'basic'; - reason = 'basic_credentials'; - } else { - reason = 'missing_basic_creds'; - } - } else if (requestedMode === 'oauth') { - const token = await getOpenSkyToken(); - if (token) { - headers.Authorization = `Bearer ${token}`; - usedMode = 'oauth'; - reason = 'oauth_token'; - } else { - reason = 'oauth_invalid_or_missing'; - } - } else if (requestedMode === 'auto') { - const token = await getOpenSkyToken(); - if (token) { - headers.Authorization = `Bearer ${token}`; - usedMode = 'oauth'; - reason = 'oauth_token'; - } else if (hasBasicCreds) { - headers.Authorization = `Basic ${Buffer.from(`${basicUser}:${basicPass}`).toString('base64')}`; - usedMode = 'basic'; - reason = 'oauth_unavailable_fallback_basic'; - } else { - reason = 'missing_oauth_and_basic_creds'; - } - } + if (requestedMode === 'basic') { + if (hasBasicCreds) { + headers.Authorization = `Basic ${Buffer.from(`${basicUser}:${basicPass}`).toString('base64')}`; + usedMode = 'basic'; + reason = 'basic_credentials'; + } else { + reason = 'missing_basic_creds'; + } + } else if (requestedMode === 'oauth') { + const token = await getOpenSkyToken(); + if (token) { + headers.Authorization = `Bearer ${token}`; + usedMode = 'oauth'; + reason = 'oauth_token'; + } else { + reason = 'oauth_invalid_or_missing'; + } + } else if (requestedMode === 'auto') { + const token = await getOpenSkyToken(); + if (token) { + headers.Authorization = `Bearer ${token}`; + usedMode = 'oauth'; + reason = 'oauth_token'; + } else if (hasBasicCreds) { + headers.Authorization = `Basic ${Buffer.from(`${basicUser}:${basicPass}`).toString('base64')}`; + usedMode = 'basic'; + reason = 'oauth_unavailable_fallback_basic'; + } else { + reason = 'missing_oauth_and_basic_creds'; + } + } - let { upstream, body } = await fetchOpenSkyStates(headers); - // Auto-mode fallback: if OAuth was rejected, retry with Basic credentials - if ( - (upstream.status === 401 || upstream.status === 403) && - requestedMode === 'auto' && - usedMode === 'oauth' && - hasBasicCreds - ) { - const retryHeaders = { - Accept: 'application/json', - Authorization: `Basic ${Buffer.from(`${basicUser}:${basicPass}`).toString('base64')}`, - }; - ({ upstream, body } = await fetchOpenSkyStates(retryHeaders)); - usedMode = 'basic'; - reason = 'oauth_rejected_fallback_basic'; - } + let { upstream, body } = await fetchOpenSkyStates(headers); + // Auto-mode fallback: if OAuth was rejected, retry with Basic credentials + if ( + (upstream.status === 401 || upstream.status === 403) && + requestedMode === 'auto' && + usedMode === 'oauth' && + hasBasicCreds + ) { + const retryHeaders = { + Accept: 'application/json', + Authorization: `Basic ${Buffer.from(`${basicUser}:${basicPass}`).toString('base64')}`, + }; + ({ upstream, body } = await fetchOpenSkyStates(retryHeaders)); + usedMode = 'basic'; + reason = 'oauth_rejected_fallback_basic'; + } - const sourceEpochMs = upstream.ok ? openSkySourceEpochMs(body) : null; + const sourceEpochMs = upstream.ok + ? openSkySourceEpochMs(body) + : null; + // Refine the reason string to reflect the actual outcome + if (upstream.ok && reason === 'forced_anonymous') { + reason = 'anonymous_ok'; + } else if ( + upstream.ok && + usedMode === 'basic' && + reason === 'basic_credentials' + ) { + reason = 'basic_ok'; + } else if ( + upstream.ok && + usedMode === 'oauth' && + reason === 'oauth_token' + ) { + reason = 'oauth_ok'; + } + + // Only cache successful responses — error responses (401/403/429/5xx) + // should not be served from cache on subsequent requests + if (upstream.ok) { + _openskyCacheBody = body; + _openskyCacheStatus = upstream.status; + _openskyCacheTime = now; + _openskyCacheSourceEpochMs = sourceEpochMs; + _openskyCacheMeta = { + requestedMode, + usedMode, + reason, + }; + // Credit governor: adapt the cache TTL to the remaining daily + // budget so a continuously-open app stretches its polls instead of + // exhausting the quota mid-day. Success also clears any cooldown. + const remaining = Number( + upstream.headers.get('x-rate-limit-remaining'), + ); + _openskyTtlMs = openskyAdaptiveTtlMs(remaining); + _openskyCooldownUntil = 0; + } + + return { + upstream, + body, + usedMode, + reason, + hasBasicCreds, + sourceEpochMs, + }; + }, + ); + let { upstream, body, usedMode, reason, hasBasicCreds, sourceEpochMs } = + await refresh.promise; if ( upstream.ok && openSkySourceIsStale(sourceEpochMs, now) && @@ -571,20 +644,16 @@ export function openSkyProxy() { 'opensky_snapshot_stale_regional_fallback', )) ) { - // Keep the last global snapshot available as a fail-soft cache, - // but do not label or render it as a fresh live result. - _openskyCacheBody = body; - _openskyCacheStatus = upstream.status; - _openskyCacheTime = now; - _openskyCacheSourceEpochMs = sourceEpochMs; - _openskyCacheMeta = { requestedMode, usedMode, reason }; return; } - if (upstream.status === 429) { - reason = 'rate_limited'; + if (upstream.status === 429 || upstream.status >= 500) { + reason = + upstream.status === 429 + ? 'rate_limited' + : `upstream_http_${upstream.status}`; // Credit governor: fetchOpenSkyStates already started the cooldown // from OpenSky's retry-after — no upstream attempts until then. - const cooldownMs = openSkyRateLimitCooldownMs(upstream); + const cooldownMs = Math.max(0, _openskyCooldownUntil - Date.now()); // Serve the last-good body instead of the 429 when we have one — // the layer keeps rendering (STALE-cued) instead of dying. if (_openskyCacheBody && _openskyCacheStatus === 200) { @@ -594,7 +663,7 @@ export function openSkyProxy() { cacheStatus: 'STALE', requestedMode, usedMode, - reason: 'rate_limited_serving_stale', + reason: `${reason}_serving_stale`, staleSeconds: (now - _openskyCacheTime) / 1000, retryAfterSeconds: cooldownMs / 1000, }), @@ -652,45 +721,6 @@ export function openSkyProxy() { } } - // Refine the reason string to reflect the actual outcome - if (upstream.ok && reason === 'forced_anonymous') { - reason = 'anonymous_ok'; - } else if ( - upstream.ok && - usedMode === 'basic' && - reason === 'basic_credentials' - ) { - reason = 'basic_ok'; - } else if ( - upstream.ok && - usedMode === 'oauth' && - reason === 'oauth_token' - ) { - reason = 'oauth_ok'; - } - - // Only cache successful responses — error responses (401/403/429/5xx) - // should not be served from cache on subsequent requests - if (upstream.ok) { - _openskyCacheBody = body; - _openskyCacheStatus = upstream.status; - _openskyCacheTime = now; - _openskyCacheSourceEpochMs = sourceEpochMs; - _openskyCacheMeta = { - requestedMode, - usedMode, - reason, - }; - // Credit governor: adapt the cache TTL to the remaining daily - // budget so a continuously-open app stretches its polls instead of - // exhausting the quota mid-day. Success also clears any cooldown. - const remaining = Number( - upstream.headers.get('x-rate-limit-remaining'), - ); - _openskyTtlMs = openskyAdaptiveTtlMs(remaining); - _openskyCooldownUntil = 0; - } - res.writeHead( upstream.status, buildOpenSkyHeaders({ diff --git a/src/tooling/liveProviders.test.mjs b/src/tooling/liveProviders.test.mjs index a60a37cea..60b2c9f25 100644 --- a/src/tooling/liveProviders.test.mjs +++ b/src/tooling/liveProviders.test.mjs @@ -583,3 +583,124 @@ test('track backfill proxy returns 502 on an oversized upstream body and caches assert.equal(res2.statusCode, 502); assert.equal(callCount, 1); }); + +test('global aircraft snapshots coalesce cold and expired callers and clear rejected flights', async (t) => { + environment(t, { OPENSKY_AUTH_MODE: 'anon' }); + t.mock.method(console, 'warn', () => {}); + t.mock.method(console, 'error', () => {}); + let now = Date.now(); + t.mock.method(Date, 'now', () => now); + for (const kind of ['opensky', 'military']) { + const module = + kind === 'opensky' + ? await import( + `../../server/providers/aircraft/opensky.js?coalesce=${Math.random()}` + ) + : await import('../../server/providers/aircraft/adsb-lol.js'); + const request = install( + kind === 'opensky' ? module.openSkyProxy() : module.adsbLolProxy(), + ); + const route = kind === 'opensky' ? '/api/flights' : '/api/military'; + let calls = 0; + let release; + let blocked; + let fail = false; + t.mock.method(globalThis, 'fetch', async () => { + calls++; + await blocked; + if (fail) throw new Error('fixture connection failed'); + return Response.json( + { time: now / 1000, states: [], ac: [] }, + { headers: { 'x-rate-limit-remaining': '5000' } }, + ); + }); + for (const phase of ['cold', 'expired', 'failed', 'recovered']) { + now += 13000; + fail = phase === 'failed'; + blocked = new Promise((resolve) => { + release = resolve; + }); + const before = calls; + const pending = Array.from({ length: 5 }, () => request(route)); + await new Promise(setImmediate); + assert.equal(calls - before, 1, `${kind} ${phase}: one refresh starts`); + release(); + const responses = await Promise.all(pending); + assert.equal(calls - before, fail && kind === 'opensky' ? 2 : 1); + assert.ok(responses.every((res) => res.statusCode === 200)); + assert.ok(responses.every((res) => res.body === responses[0].body)); + } + } +}); + +test('OpenSky 5xx retains dated last-good data, cancels error bodies and cools down before recovery', async (t) => { + environment(t, { OPENSKY_AUTH_MODE: 'anon' }); + let now = Date.now(); + t.mock.method(Date, 'now', () => now); + for (const status of [502, 503, 504]) { + const module = await import( + `../../server/providers/aircraft/opensky.js?server-error=${status}` + ); + const request = install(module.openSkyProxy()); + let fail = false; + let calls = 0; + let cancelled = false; + t.mock.method(globalThis, 'fetch', async () => { + calls++; + if (fail) + return new Response( + new ReadableStream({ + cancel() { + cancelled = true; + }, + }), + { status }, + ); + return Response.json( + { time: now / 1000, states: [] }, + { headers: { 'x-rate-limit-remaining': '5000' } }, + ); + }); + const good = await request('/api/flights'); + now += 10000; + fail = true; + const stale = await request('/api/flights'); + assert.equal(stale.statusCode, 200); + assert.equal(stale.body, good.body); + assert.equal(stale.headers['x-opensky-cache'], 'STALE'); + assert.equal( + stale.headers['x-opensky-auth-reason'], + `upstream_http_${status}_serving_stale`, + ); + assert.equal(stale.headers['x-opensky-stale-seconds'], '10'); + assert.equal(cancelled, true); + const cooling = await request('/api/flights'); + assert.equal(cooling.body, good.body); + assert.equal( + cooling.headers['x-opensky-auth-reason'], + `upstream_http_${status}_serving_stale`, + ); + assert.equal(calls, 2); + now += 16000; + fail = false; + assert.equal( + (await request('/api/flights')).headers['x-opensky-cache'], + 'MISS', + ); + assert.equal(calls, 3); + } + const coldModule = + await import('../../server/providers/aircraft/opensky.js?cold-server-error'); + let calls = 0; + t.mock.method(globalThis, 'fetch', async () => { + calls++; + return new Response('failure', { status: 503 }); + }); + const cold = install(coldModule.openSkyProxy()); + for (let i = 0; i < 2; i++) { + const response = await cold('/api/flights'); + assert.equal(response.statusCode, 503); + assert.match(JSON.parse(response.body).error, /OpenSky/); + } + assert.equal(calls, 1); +}); From e20a56bcb0f980a000470a995a35f4e5b60335d9 Mon Sep 17 00:00:00 2001 From: Said Date: Tue, 20 Oct 2026 00:25:43 +0100 Subject: [PATCH 04/25] docs: record aircraft fix --- changelog.d/1269-aircraft-issue-fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1269-aircraft-issue-fix.md diff --git a/changelog.d/1269-aircraft-issue-fix.md b/changelog.d/1269-aircraft-issue-fix.md new file mode 100644 index 000000000..8984a9141 --- /dev/null +++ b/changelog.d/1269-aircraft-issue-fix.md @@ -0,0 +1 @@ +- Coalesce refreshes and cool down opensky failures (asturksever, #1269). From d49877ad2e44e37f97e4c300bb2088e0c9786352 Mon Sep 17 00:00:00 2001 From: Said Date: Tue, 20 Oct 2026 00:38:09 +0100 Subject: [PATCH 05/25] fix(share): reject inherited style keys during restore --- docs/CURRENT-STATE.md | 5 +++++ src/sharelink.celestial.test.mjs | 17 +++++++++++++++++ src/sharelink.js | 4 +++- src/ui/visualSettings.js | 5 +++++ src/view/index.js | 4 +++- src/view/index.test.mjs | 17 +++++++++++++++++ 6 files changed, 50 insertions(+), 2 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 55b7a63ab..74994075a 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1,5 +1,10 @@ # God's Eye View Current State +Share links and structured views accept only own keys in their style registries. +Inherited names such as `constructor` and `__proto__` fall back to the existing +normal/null defaults without interrupting other restored fields. Programmatic +visual-style calls ignore unknown or non-string values before mutating state. + Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is canonical; `greatCircleMeters` is its exact metre conversion. Missing, diff --git a/src/sharelink.celestial.test.mjs b/src/sharelink.celestial.test.mjs index e862aba66..13dedd709 100644 --- a/src/sharelink.celestial.test.mjs +++ b/src/sharelink.celestial.test.mjs @@ -778,3 +778,20 @@ test('visual input listeners are revoked before asynchronous UI teardown', () => assert.match(VisualSettings.prototype.stop.toString(), /this\._styleParameters\?\.destroy\(\)/); assert.match(VisualSettings.prototype.stop.toString(), /this\._visualEffects\.stop\(\)/); }); + + +test('share restore rejects inherited styles while continuing later fields', () => { + for (const style of ['constructor', '__proto__', 'toString', 'hasOwnProperty', 'unknown']) { + const restored = makeManager(`#lat=10&lon=20&style=${style}&cr=1`).parseInitialHash(); + assert.equal(restored.style, 'normal'); + assert.equal(restored.celestialRing, true); + } +}); + +test('programmatic invalid styles are ignored before changing visual state', () => { + const target = { activeStyle: 'normal' }; + for (const value of ['constructor', '__proto__', 'toString', 'hasOwnProperty', 'unknown', {}, null, () => {}]) { + assert.doesNotThrow(() => VisualSettings.prototype.setStyle.call(target, value)); + assert.equal(target.activeStyle, 'normal'); + } +}); diff --git a/src/sharelink.js b/src/sharelink.js index 6072fb1bd..7e0e78b35 100644 --- a/src/sharelink.js +++ b/src/sharelink.js @@ -175,7 +175,9 @@ export class ShareLinkManager { parseOr(params.get('dd'), 50), 50, ); - const style = URL_TO_STYLE[params.get('style')] || 'normal'; + const style = Object.hasOwn(URL_TO_STYLE, params.get('style')) + ? URL_TO_STYLE[params.get('style')] + : 'normal'; const decodedLayerState = decodeLayerStateParams(params); const state = { lat, diff --git a/src/ui/visualSettings.js b/src/ui/visualSettings.js index 2aab58f10..b80e4452f 100644 --- a/src/ui/visualSettings.js +++ b/src/ui/visualSettings.js @@ -1539,6 +1539,11 @@ export class VisualSettings { preserveCyberRestore = false, } = {}, ) { + if ( + typeof styleName !== 'string' || + (styleName !== 'normal' && !Object.hasOwn(STYLES, styleName)) + ) + return; const { setDetectionStyle } = this.services; if (!restore) this.shareLinkManager?.claimRestoreLane?.('visual'); if (this.hud.getVariant() === 'cyber') { diff --git a/src/view/index.js b/src/view/index.js index 8ab56d5a4..fc5385aaa 100644 --- a/src/view/index.js +++ b/src/view/index.js @@ -370,7 +370,9 @@ export function viewFromParams(params) { pitch_deg: number('pitch'), }, layers: state?.enabledLayerIds ?? [], - style: STYLES_BY_URL_NAME[params.get('style')] ?? null, + style: Object.hasOwn(STYLES_BY_URL_NAME, params.get('style')) + ? STYLES_BY_URL_NAME[params.get('style')] + : null, map: VIEW_MAPS.includes(map) ? map : null, follow, annotations: annotationsFromParams(params), diff --git a/src/view/index.test.mjs b/src/view/index.test.mjs index 818feb1ba..34299c498 100644 --- a/src/view/index.test.mjs +++ b/src/view/index.test.mjs @@ -126,3 +126,20 @@ test('annotations travel in the link, keeping only what the app draws', async () // Too long for a link: the view keeps them, the link leaves them out. assert.equal(viewToParams(many).has('an'), false); }); + +test('inherited and unknown style keys do not corrupt a restored view', () => { + for (const style of [ + 'constructor', + '__proto__', + 'toString', + 'hasOwnProperty', + 'unknown', + ]) { + const view = viewFromParams( + new URLSearchParams({ lat: '10', lon: '20', style, map: 'osm' }), + ); + assert.equal(view.style, null); + assert.equal(view.map, 'osm'); + assert.equal(view.camera.lat, 10); + } +}); From 53b6eae1222cc6852d73e6be8596697b564b4819 Mon Sep 17 00:00:00 2001 From: Said Date: Tue, 20 Oct 2026 00:38:18 +0100 Subject: [PATCH 06/25] docs: record share fix --- changelog.d/1271-share-issue-fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1271-share-issue-fix.md diff --git a/changelog.d/1271-share-issue-fix.md b/changelog.d/1271-share-issue-fix.md new file mode 100644 index 000000000..7a01330ba --- /dev/null +++ b/changelog.d/1271-share-issue-fix.md @@ -0,0 +1 @@ +- Reject inherited style keys during restore (asturksever, #1271). From 0cb6857229b497043177db267ff07d95a9fa7f99 Mon Sep 17 00:00:00 2001 From: Said Date: Tue, 20 Oct 2026 00:55:40 +0100 Subject: [PATCH 07/25] fix(weather): preserve enable during first-load replacement --- docs/CURRENT-STATE.md | 2 ++ src/layers/weather/index.js | 15 +++++++++---- src/layers/weather/weather.test.mjs | 32 +++++++++++++++++++++++++++- src/layers/wind/index.js | 5 +++-- src/layers/wind/index.test.mjs | 33 +++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 7 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 55b7a63ab..2309269b8 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1,5 +1,7 @@ # God's Eye View Current State +Wind and weather parameter changes may supersede a first-load request without rejecting the layer’s enable transaction. Obsolete responses cannot publish data; a replacement request remains visible through the loading state. Explicit caller cancellation, disable, and destroy still reject the obsolete update. Source failures continue to report their existing unavailable state. + Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is canonical; `greatCircleMeters` is its exact metre conversion. Missing, diff --git a/src/layers/weather/index.js b/src/layers/weather/index.js index db62ea98a..188955090 100644 --- a/src/layers/weather/index.js +++ b/src/layers/weather/index.js @@ -357,7 +357,7 @@ export function createWeatherLayer({ signal: controller.signal, }); if (!enabled || controller.signal.aborted || request !== controller) - return false; + return enabled && !signal?.aborted && request !== controller; if (snapshot.unavailable) { error = msg( 'Weather source unavailable; previous observation retained', @@ -376,7 +376,9 @@ export function createWeatherLayer({ ) await show(snapshot.latest, controller.signal); return ( - !controller.signal.aborted && request === controller && enabled + enabled && + !signal?.aborted && + (!controller.signal.aborted || request !== controller) ); } const time = @@ -388,9 +390,14 @@ export function createWeatherLayer({ (satellite && rendering.getDiagnostics().infrared !== infrared) ) await show(time, controller.signal); - return !controller.signal.aborted && request === controller && enabled; + return ( + enabled && + !signal?.aborted && + (!controller.signal.aborted || request !== controller) + ); } catch (cause) { - if (controller.signal.aborted || request !== controller) return false; + if (controller.signal.aborted || request !== controller) + return enabled && !signal?.aborted && request !== controller; error = cause?.message || msg('Weather unavailable'); stop(); return true; diff --git a/src/layers/weather/weather.test.mjs b/src/layers/weather/weather.test.mjs index 1a4ba5177..38668909a 100644 --- a/src/layers/weather/weather.test.mjs +++ b/src/layers/weather/weather.test.mjs @@ -694,7 +694,11 @@ test('superseding a pending stage preserves the newer manifest loading state', a await flush(); const newer = h.layer.update(); await flush(); - assert.equal(await old, false); + assert.equal( + await old, + true, + 'replacement is accepted by the enable lifecycle', + ); assert.equal( h.layer.getStats().loading, true, @@ -2311,3 +2315,29 @@ for (const product of ['radar', 'clouds-regional', 'lightning']) { assert.equal(h.viewer.camera.moveEnd.size, 0); }); } + +test('a product replacement accepts supersession but never paints the obsolete first response', async () => { + const pending = []; + const h = layerHarness({ + id: 'weather-satellite', + feed: { + getSnapshot: (args) => { + const task = deferred(); + pending.push({ ...args, ...task }); + return task.promise; + }, + }, + }); + const first = h.layer.update(); + h.layer.setParams({ product: 'clouds' }); + assert.equal(pending.length, 2); + pending[0].resolve({ ...snapshot, product: 'clouds-regional' }); + assert.equal(await first, true); + assert.equal(h.stages.length, 0); + pending[1].resolve({ ...snapshot, product: 'clouds' }); + await flush(); + h.stages.at(-1).finish(); + await flush(); + assert.equal(h.layer.getParams().product, 'clouds'); + h.layer.destroy(); +}); diff --git a/src/layers/wind/index.js b/src/layers/wind/index.js index c76e2501c..aa6161f06 100644 --- a/src/layers/wind/index.js +++ b/src/layers/wind/index.js @@ -180,7 +180,7 @@ export function createWindLayer({ overlay: requestedScalar(), }); if (!enabled || controller.signal.aborted || request !== controller) - return false; + return enabled && !signal?.aborted && request !== controller; manifest = snapshot; error = null; if (sampledPosition) sample(sampledPosition); @@ -194,7 +194,8 @@ export function createWindLayer({ } return true; } catch (cause) { - if (controller.signal.aborted || request !== controller) return false; + if (controller.signal.aborted || request !== controller) + return enabled && !signal?.aborted && request !== controller; error = cause?.message || msg('Wind source unavailable'); return true; } finally { diff --git a/src/layers/wind/index.test.mjs b/src/layers/wind/index.test.mjs index ac843f62a..3b5e09697 100644 --- a/src/layers/wind/index.test.mjs +++ b/src/layers/wind/index.test.mjs @@ -412,3 +412,36 @@ test('wind unit chips appear only alongside a speed legend, including canvas tra for (const overlay of ['pressure', 'temperature', 'none']) { layer.setParams({ overlay }); assert.deepEqual(unitChips(), []); } layer.destroy(); }); + +test('a parameter replacement during manager enable does not roll the layer back', async () => { + const { LayerLifecycle } = await import('../../data/lifecycle.js'); + const pending = []; + const rendering = Object.fromEntries( + ['attach', 'start', 'stop', 'clear', 'destroy', 'setField'].map((name) => [ + name, + () => {}, + ]), + ); + const layer = createWindLayer({ + feed: { + getSnapshot: (args) => + new Promise((resolve) => pending.push({ ...args, resolve })), + }, + createRendering: () => rendering, + }); + const manager = new LayerLifecycle({ container: {} }); + manager.register(layer); + const enabled = manager.setEnabled('wind', true); + for (let i = 0; i < 20 && !pending.length; i++) await Promise.resolve(); + layer.setParams({ model: 'ifs' }); + await Promise.resolve(); + pending[0].resolve(snapshot('gfs')); + assert.equal(await enabled, true); + assert.equal(manager.isEnabled('wind'), true); + assert.equal(layer.getStats().loading, true); + pending[1].resolve(snapshot('ifs')); + for (let i = 0; i < 10; i++) await Promise.resolve(); + assert.equal(layer.getStats().model, 'IFS'); + assert.equal(layer.getStats().count, 4); + await manager.destroyAll(); +}); From 2f23bbe87f3c183d311dd758c74c3b721a966650 Mon Sep 17 00:00:00 2001 From: Said Date: Tue, 20 Oct 2026 00:56:00 +0100 Subject: [PATCH 08/25] docs: record forecast fix --- changelog.d/1275-forecast-issue-fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1275-forecast-issue-fix.md diff --git a/changelog.d/1275-forecast-issue-fix.md b/changelog.d/1275-forecast-issue-fix.md new file mode 100644 index 000000000..9308249e2 --- /dev/null +++ b/changelog.d/1275-forecast-issue-fix.md @@ -0,0 +1 @@ +- Preserve enable during first-load replacement (asturksever, #1275). From 7860cbf59e05549604c08414a086dacf3f5adf2b Mon Sep 17 00:00:00 2001 From: Said Date: Tue, 20 Oct 2026 00:57:24 +0100 Subject: [PATCH 09/25] fix(terrain): bound each height request to one upstream chunk --- docs/CURRENT-STATE.md | 5 +++ server/providers/terrain.js | 10 +++--- src/terrainHeightsBounds.test.mjs | 21 ++++++++++++ src/tooling/environmentProviders.test.mjs | 41 ++++++++++++----------- 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 55b7a63ab..e5c4bb9f9 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1,5 +1,10 @@ # God's Eye View Current State +Terrain height requests accept at most 64 points, matching the browser service's +existing batch size and one bounded upstream chunk. Larger requests return HTTP +413 before disk-cache loading or upstream work; malformed coordinates remain 400. +The browser continues to split larger workloads into sequential 64-point requests. + Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is canonical; `greatCircleMeters` is its exact metre conversion. Missing, diff --git a/server/providers/terrain.js b/server/providers/terrain.js index 32083d23a..c678ab7c0 100644 --- a/server/providers/terrain.js +++ b/server/providers/terrain.js @@ -20,8 +20,8 @@ import { admitSameSiteKeyless } from './common/same-site.js'; * cache and serve-stale shape. Cache entries and stale fallback are keyed per * 5dp point, so reordered and partially overlapping batches reuse prior work. * Only missing/stale points go upstream; the response is rebuilt in exact - * request order. Larger requests are chunked sequentially, and one failing - * chunk does not discard the chunks that resolved. + * request order. Each request is bounded to one 64-point upstream chunk; + * larger client workloads are split by the browser terrain service. * * Bounds, because every distinct 5dp point is its own key: coordinates must be * in WGS-84 range (rejected at parse), the point cache has an entry ceiling @@ -55,7 +55,7 @@ export function terrainHeightsProxy(options = {}) { // 2026-08-21). 64 points costs 5.6-12 s, inside the timeout at both ends of // that range. const UPSTREAM_CHUNK = 64; - const MAX_POINTS = 2000; + const MAX_POINTS = UPSTREAM_CHUNK; /** * Entry ceiling for the point cache, and with it the file written from it. * @@ -255,7 +255,6 @@ export function terrainHeightsProxy(options = {}) { res.end(JSON.stringify({ error: 'Rate limit exceeded' })); return; } - await loadDiskOnce(); const parsedUrl = new URL(req.url || '', 'http://internal'); const rawPoints = parsedUrl.searchParams.get('points'); const points = parseTerrainPoints(rawPoints); @@ -267,12 +266,13 @@ export function terrainHeightsProxy(options = {}) { return; } if (points.length > MAX_POINTS) { - send(500, { + send(413, { error: `too many points (${points.length}); max ${MAX_POINTS} per request`, }); return; } + await loadDiskOnce(); const outcome = await resolveTerrainHeightRequest({ points, cache: mem, diff --git a/src/terrainHeightsBounds.test.mjs b/src/terrainHeightsBounds.test.mjs index cd1a9ad36..3746f4fe4 100644 --- a/src/terrainHeightsBounds.test.mjs +++ b/src/terrainHeightsBounds.test.mjs @@ -366,3 +366,24 @@ test('a second client keeps its own quota', async (t) => { }); assert.notEqual(neighbour.statusCode, 429); }); + +test('one 64-point chunk is accepted; larger batches fail before cache or upstream work', async (t) => { + const root = await mkdtemp(path.join(tmpdir(), 'terrain-batch-limit-')); + t.after(() => rm(root, { recursive: true, force: true })); + const asked = stubTerrainUpstream(t); + const route = install( + terrainHeightsProxy({ cachePath: path.join(root, 'cache.json') }), + ).get('/api/terrain/heights'); + const points = (count) => + Array.from({ length: count }, (_, i) => `${i / 1000},30`).join(';'); + for (const count of [65, 2001]) { + const response = await request(route, { url: `/?points=${points(count)}` }); + assert.equal(response.statusCode, 413); + assert.match(response.body.error, /max 64/); + assert.equal(asked.length, 0); + } + const response = await request(route, { url: `/?points=${points(64)}` }); + assert.equal(response.statusCode, 200); + assert.equal(response.body.results.length, 64); + assert.equal(asked.length, 64); +}); diff --git a/src/tooling/environmentProviders.test.mjs b/src/tooling/environmentProviders.test.mjs index b6faeb34c..bbb0e9d6e 100644 --- a/src/tooling/environmentProviders.test.mjs +++ b/src/tooling/environmentProviders.test.mjs @@ -98,7 +98,7 @@ test('standalone composition mounts every extracted provider exactly once withou assert.equal(plugins.filter((p) => p.name === factory().name).length, 1); }); -test('terrain middleware chunks missing points and reconstructs repeated/reordered requests from cache', async (t) => { +test('terrain middleware bounds missing points and reconstructs repeated/reordered requests from cache', async (t) => { isolate(t); let calls = 0; const sizes = []; @@ -116,25 +116,23 @@ test('terrain middleware chunks missing points and reconstructs repeated/reorder }); }); const request = install(terrainHeightsProxy()); - const points = Array.from({ length: 257 }, (_, i) => `${i / 100},1`); + const points = Array.from({ length: 64 }, (_, i) => `${i / 100},1`); const res = await request('/?points=' + points.join(';')); assert.equal(res.status, 200); - // Tracks UPSTREAM_CHUNK in server/providers/terrain.js (64): 257 points - // split into four full chunks and a remainder. - assert.deepEqual(sizes, [64, 64, 64, 64, 1]); - const reordered = await request('/?points=2.56,1;0,1;2.56,1'); + assert.deepEqual(sizes, [64]); + const reordered = await request('/?points=0.63,1;0,1;0.63,1'); assert.deepEqual(json(reordered), { - results: [{ ellipsoid: 102.56 }, { ellipsoid: 100 }, { ellipsoid: 102.56 }], + results: [{ ellipsoid: 100.63 }, { ellipsoid: 100 }, { ellipsoid: 100.63 }], }); - // Five chunks for the first batch; the reordered request is fully cached. - assert.equal(calls, 5); + // One bounded batch; the reordered request is fully cached. + assert.equal(calls, 1); assert.equal((await request('/?points=invalid')).status, 400); assert.equal( (await request('/?points=' + Array(2001).fill('0,1').join(';'))).status, - 500, + 413, ); - // Rejected requests add no upstream calls: still the five from the first batch. - assert.equal(calls, 5); + // Rejected requests add no upstream calls. + assert.equal(calls, 1); }); test('terrain middleware migrates valid legacy disk points without fabricating omitted heights', async (t) => { @@ -388,21 +386,26 @@ test('terrain middleware retains successful chunks around a failure and retries }); const request = install(terrainHeightsProxy()); const points = Array.from({ length: 130 }, (_, i) => `${i / 100},1`); - const url = '/?points=' + points.join(';'); - assert.equal((await request(url)).status, 502); + const urls = [0, 64, 128].map( + (start) => '/?points=' + points.slice(start, start + 64).join(';'), + ); assert.deepEqual( - requests.map((p) => p.length), - [64, 64, 2], + await Promise.all(urls.map(async (url) => (await request(url)).status)), + [200, 502, 200], + ); + assert.deepEqual( + requests.map((p) => p.length).sort((a, b) => a - b), + [2, 64, 64], ); fail = false; - const recovered = await request(url); - assert.equal(recovered.status, 200); + const recovered = await Promise.all(urls.map((url) => request(url))); + assert.ok(recovered.every((response) => response.status === 200)); assert.deepEqual( requests[3], Array.from({ length: 64 }, (_, i) => [(i + 64) / 100, 1]), ); assert.deepEqual( - json(recovered).results, + recovered.flatMap((response) => json(response).results), Array.from({ length: 130 }, (_, i) => ({ ellipsoid: i / 100 + 100 })), ); assert.equal(requests.length, 4); From f436d72d1bccf20f95141fb470d497a35831b157 Mon Sep 17 00:00:00 2001 From: Said Date: Tue, 20 Oct 2026 00:57:54 +0100 Subject: [PATCH 10/25] docs: record terrain fix --- changelog.d/1276-terrain-issue-fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1276-terrain-issue-fix.md diff --git a/changelog.d/1276-terrain-issue-fix.md b/changelog.d/1276-terrain-issue-fix.md new file mode 100644 index 000000000..17e8f4b50 --- /dev/null +++ b/changelog.d/1276-terrain-issue-fix.md @@ -0,0 +1 @@ +- Bound each height request to one upstream chunk (asturksever, #1276). From d6f89e3cfd9b27e21c2ee5ad0307107ed46c079f Mon Sep 17 00:00:00 2001 From: Said Date: Tue, 20 Oct 2026 14:01:22 +0100 Subject: [PATCH 11/25] fix: release weather camera listeners while disabled --- changelog.d/1275-forecast-issue-fix.md | 1 + docs/CURRENT-STATE.md | 1 + scripts/qa-weather-teardown.mjs | 28 ++++++++++++++++++++++---- src/layers/weather/index.js | 12 +++++++---- src/layers/weather/weather.test.mjs | 15 ++++++++++++++ 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/changelog.d/1275-forecast-issue-fix.md b/changelog.d/1275-forecast-issue-fix.md index 9308249e2..6168e7f1f 100644 --- a/changelog.d/1275-forecast-issue-fix.md +++ b/changelog.d/1275-forecast-issue-fix.md @@ -1 +1,2 @@ - Preserve enable during first-load replacement (asturksever, #1275). +- Release disabled weather camera listeners and make teardown resource leaks fail the browser probe (asturksever, #1275). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 2309269b8..00e533540 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -253,6 +253,7 @@ catalog construction owns each instance. Wind animates one forecast without advancing forecast time. Separate Weather observation layers provide radar and satellite history; none claims measured cloud volume. +Weather imagery releases its camera move listener on disable and registers it once on re-enable. Disabling also clears active imagery work. Weather imagery drapes onto the globe basemap. On photorealistic 3D Tiles each observed product is instead a raised, translucent shell: one rectangle over the product bounds at a fixed height (global infrared 5.5 km, regional infrared diff --git a/scripts/qa-weather-teardown.mjs b/scripts/qa-weather-teardown.mjs index b64d1d59c..c81a7bec1 100644 --- a/scripts/qa-weather-teardown.mjs +++ b/scripts/qa-weather-teardown.mjs @@ -141,7 +141,7 @@ const snapshot = (page) => ), }, weather, - windCells: wind.gpu?.pathCount ?? wind.paths ?? null, + windCells: wind.gpu?.pathCount ?? wind.paths ?? 0, enabled: [...dm.layers].filter(([, e]) => e.enabled).map(([id]) => id), }; }); @@ -220,7 +220,7 @@ try { const baseline = await snapshot(a.page); console.log('baseline', JSON.stringify(baseline)); for (const id of WEATHER) await setLayer(a.page, id, true); - await a.page + const weatherReady = await a.page .waitForFunction( () => ['weather-radar', 'weather-satellite', 'weather-lightning'].every( @@ -233,7 +233,13 @@ try { ), { timeout: 120_000 }, ) - .catch(() => console.log(' !! weather not ready')); + .then( + () => true, + () => false, + ); + results.weatherReady = weatherReady; + if (!weatherReady) + console.log(' !! weather not ready — feature coverage incomplete'); await sleep(2000); // exercise: history playback, wind reading, storm selection, product switch await a.page.evaluate(() => { @@ -331,11 +337,25 @@ try { 'errors (control run):', results.errorsB.length ? results.errorsB : 'none', ); + // Resource ownership is an acceptance check; provider availability remains + // separately reported diagnostic evidence in both runs. + if ( + !weatherReady || + Object.keys(leak).length || + [...a.errors, ...b.errors].some((e) => e.startsWith('pageerror:')) + ) { + process.exitCode = 1; + } +} catch (error) { + results.failure = { name: error.name, message: error.message }; + throw error; +} finally { + // Preserve completed resource evidence even if a later provider or browser + // phase aborts. A partial run still exits nonzero. writeFileSync( path.join(outDir, 'results.json'), JSON.stringify(results, null, 2), ); console.log(`wrote ${path.join(outDir, 'results.json')}`); -} finally { await browser.close(); } diff --git a/src/layers/weather/index.js b/src/layers/weather/index.js index 188955090..757764fe6 100644 --- a/src/layers/weather/index.js +++ b/src/layers/weather/index.js @@ -296,10 +296,6 @@ export function createWeatherLayer({ motion?.addEventListener?.('change', onVisibility); documentRef?.addEventListener?.('visibilitychange', onVisibility); registerClock(); - removeCamera = viewer.camera.moveEnd?.addEventListener(() => { - checkHost(); - if (enabled) notify(); - }); }, attachShellServices(services) { imageryHost = @@ -315,10 +311,18 @@ export function createWeatherLayer({ }, enable() { enabled = true; + if (!removeCamera) { + removeCamera = viewer.camera.moveEnd?.addEventListener(() => { + checkHost(); + notify(); + }); + } registerClock(); }, disable() { enabled = false; + removeCamera?.(); + removeCamera = null; unregisterClock?.(); unregisterClock = null; ++generation; diff --git a/src/layers/weather/weather.test.mjs b/src/layers/weather/weather.test.mjs index 38668909a..486d42520 100644 --- a/src/layers/weather/weather.test.mjs +++ b/src/layers/weather/weather.test.mjs @@ -2341,3 +2341,18 @@ test('a product replacement accepts supersession but never paints the obsolete f assert.equal(h.layer.getParams().product, 'clouds'); h.layer.destroy(); }); + +test('disabled weather releases its camera listener and re-enabling binds only once', () => { + const h = layerHarness(); + h.layer.disable(); + assert.equal(h.moveEnd.size, 0, 'disabled layers own no active camera work'); + h.layer.enable(); + h.layer.enable(); + assert.equal(h.moveEnd.size, 1); + h.layer.disable(); + assert.equal(h.moveEnd.size, 0); + h.layer.enable(); + assert.equal(h.moveEnd.size, 1); + h.layer.destroy(); + assert.equal(h.moveEnd.size, 0); +}); From eb2ac9197d9b0f76ae46d8fcf7fc849638df25e5 Mon Sep 17 00:00:00 2001 From: Said Date: Tue, 20 Oct 2026 14:37:05 +0100 Subject: [PATCH 12/25] test: keep weather teardown browser actively rendering --- changelog.d/1275-forecast-issue-fix.md | 1 + scripts/qa-weather-teardown.mjs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/changelog.d/1275-forecast-issue-fix.md b/changelog.d/1275-forecast-issue-fix.md index 6168e7f1f..2013bc4f2 100644 --- a/changelog.d/1275-forecast-issue-fix.md +++ b/changelog.d/1275-forecast-issue-fix.md @@ -1,2 +1,3 @@ - Preserve enable during first-load replacement (asturksever, #1275). - Release disabled weather camera listeners and make teardown resource leaks fail the browser probe (asturksever, #1275). +- Keep visible-page weather teardown probes focused through the browser testing API (asturksever, #1275). diff --git a/scripts/qa-weather-teardown.mjs b/scripts/qa-weather-teardown.mjs index c81a7bec1..bf7b00269 100644 --- a/scripts/qa-weather-teardown.mjs +++ b/scripts/qa-weather-teardown.mjs @@ -61,6 +61,10 @@ const browser = await puppeteer.launch({ const boot = async () => { const page = await browser.newPage(); + // This probe measures active rendering; an OS-minimized window must not + // silently exercise the application's intentional hidden-page pause. + const focus = await page.createCDPSession(); + await focus.send('Emulation.setFocusEmulationEnabled', { enabled: true }); await page.setViewport({ width: 1400, height: 920, deviceScaleFactor: 1 }); const errors = []; page.on('pageerror', (e) => From 3eb585fe49ab340cdd7e068a4afefcc474a84a3d Mon Sep 17 00:00:00 2001 From: Enzo Abyie Date: Wed, 21 Oct 2026 12:52:34 +0400 Subject: [PATCH 13/25] i18n(zh-CN): Simplified Chinese strings from #914 The Simplified Chinese locale by abiyeenzo (#914), converted from its namespaced JS catalogs to the English-keyed zh-CN.json: 891 entries whose English still matches the current UI. --- src/i18n/locales/zh-CN.json | 907 ++++++++++++++++++++++++++++++++++++ 1 file changed, 907 insertions(+) create mode 100644 src/i18n/locales/zh-CN.json diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json new file mode 100644 index 000000000..cbedad420 --- /dev/null +++ b/src/i18n/locales/zh-CN.json @@ -0,0 +1,907 @@ +{ + "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...": "正在初始化系统…", + "Close data attribution": "关闭数据来源说明", + "UNAVAILABLE": "不可用", + "SOURCE": "来源", + "Overpass rate-limited": "Overpass 已限流", + "Overpass timed out": "Overpass 超时", + "Overpass could not complete the query": "Overpass 无法完成查询", + "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": "已加载地图站点", + "Mapped installation": "地图标注设施", + "military flight": "军用航班", + "feed unavailable": "数据源不可用", + "feed stale": "数据源过期", + "observed or mapped nearby context": "附近有观测或地图标注的上下文", + "no observed or mapped objects in current feeds": "当前数据源中没有观测或地图标注的对象", + "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": "其他", + "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": "雪景", + "LOADING": "加载中", + "DEGRADED": "降级", + "STALE": "过期", + "PARTIAL": "部分", + "FALLBACK": "备用", + "{state} {layers}": "{state} {layers}", + "SUMMARY": "摘要", + "Awaiting telemetry...": "等待遥测数据…", + "REC": "REC", + "AIS: --": "AIS: --", + "POWER UP · {count} KEYS WAITING": { + "other": "启动 · {count} 个密钥待配置" + }, + "POWERED UP": "已启动", + "Save failed ({status}).": "保存失败({status})。", + "REMOVE": "移除", + "Remove {title} from this app's saved keys": "从本应用已保存的密钥中移除 {title}", + "your app configuration": "你的应用配置", + "your local .env": "你的本地 .env", + "Saving…": "正在保存…", + "Save failed: {detail}": "保存失败:{detail}", + "Paste at least one key first.": "请先至少粘贴一个密钥。", + "Saved to {store}. Restarting — this page reloads itself.": "已保存到{store}。正在重启——本页面将自动重新加载。", + "Remove this key from your saved configuration?": "要从已保存的配置中移除此密钥吗?", + "Removed from {store}. Restarting — this page reloads itself.": "已从{store}中移除。正在重启——本页面将自动重新加载。", + "Places context + Street View fallback; optional separate key": "地点上下文 + 街景备用;可选的独立密钥", + "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)": "更多航班轮询额度(匿名也可使用)", + "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": "下一个", + "Open-source mapped/observed context. Missing broadcasts, unloaded map areas, or unmapped sites are not evidence of absence.": "开源的地图标注/观测上下文。缺失的广播、未加载的地图区域或未标注的站点不能作为不存在的证据。", + "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...": "正在扫描附近系统…", + "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}°": "FOV {value}°", + "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} 上下文", + "{distance} · {duration} · {mode}": "{distance} · {duration} · {mode}", + "Focus fire detection {title}, {details}": "聚焦火点探测 {title},{details}", + "{value} conf": "置信度 {value}", + "{value} ago": "{value} 前", + "sensor n/a": "传感器 n/a", + "FIRE · {frp} MW": "火点 · {frp} MW", + "NIGHT": "夜间", + "▲ {frp} MW": "▲ {frp} MW", + "FIRE": "火点", + "FIRES": "火点", + "max {value} MW": "最大 {value} MW", + "new {value}": "新增 {value}", + "just now": "刚刚", + "<1m ago": "不到 1 分钟前", + "{count}m ago": "{count} 分钟前", + "{count}h ago": "{count} 小时前", + "STALE · cached {age}": "过期 · 缓存于 {age}", + "refreshing...": "刷新中…", + "loading...": "加载中…", + "KEY REQUIRED": "需要密钥", + "LIVE · updated {age}": "实时 · 更新于 {age}", + "loading mapped installation context": "正在加载地图标注设施上下文", + "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}": "T−{seconds}", + "Preparing launch site": "正在准备发射场", + "Liftoff": "点火升空", + "Ascent replay": "上升回放", + "Orbit replay": "轨道回放", + ", paused": ",已暂停", + "listening": "聆听中", + "Alt unknown": "高度未知", + "Type unknown": "类型未知", + "Reg unknown": "注册号未知", + "Operator unknown": "运营方未知", + "News": "新闻", + "Talk": "谈话", + "Weather / Emergency": "天气 / 应急", + "Public Safety": "公共安全", + "Aviation / Marine": "航空 / 海事", + "Traffic / Transit": "交通 / 公交", + "Music": "音乐", + "Other": "其他", + "Alternative": "另类", + "Ambient": "氛围", + "Blues": "蓝调", + "Classical": "古典", + "Country": "乡村", + "Dance": "舞曲", + "Electronic": "电子", + "Folk": "民谣", + "Funk": "放克", + "Hip-Hop": "嘻哈", + "House": "House", + "Indie": "独立音乐", + "Jazz": "爵士", + "Latin": "拉丁", + "Metal": "金属乐", + "Oldies": "怀旧金曲", + "Pop": "流行", + "Punk": "朋克", + "R&B": "R&B", + "Reggae": "雷鬼", + "Rock": "摇滚", + "Soul": "灵魂乐", + "Techno": "Techno", + "Trance": "Trance", + "World": "世界音乐", + "cloud unknown": "云量未知", + "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": "密集", + "Selected": "所选", + "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}%", + "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 密钥以获取实时数据", + "AIS: {name}": "AIS: {name}", + "{type} SPD: {speed} HDG: {heading}": "{type} SPD: {speed} HDG: {heading}", + "VESSEL": "船舶", + "POS: LIVE": "位置:实时", + "POS: {time}Z": "位置:{time}Z", + "awaiting first AIS position…": "等待首个 AIS 位置…", + "awaiting usable AIS positions…": "等待可用的 AIS 位置…", + "awaiting first AIS message…": "等待首条 AIS 消息…", + "{minutes}m ago": "{minutes} 分钟前", + "{state} · {time} · {lag}": "{state} · {time} · {lag}", + "{state}: {time}": "{state}:{time}", + "loading": "加载中", + "Wind": "风", + "Resume": "继续", + "Pause": "暂停", + "Error: {message}": "错误:{message}", + "Needs {envVars} — add it in Provider Settings": "需要 {envVars},请在服务商设置中添加", + "{label} is unavailable": "{label} 不可用", + "This map stack": "此地图组合", + "{label} unavailable: {hint}": "{label} 不可用:{hint}", + "Visible map targets": "可见的地图目标", + "Source": "来源", + "Ready": "就绪", + "Loaded: {scene} / {shot}": "已加载:{scene} / {shot}", + "Cannot capture shot: camera not ready": "无法截取镜头:相机未就绪", + "Captured: {scene} / {shot}": "已截取:{scene} / {shot}", + "Select a shot first": "请先选择一个镜头", + "Updated: {scene} / {shot}": "已更新:{scene} / {shot}", + "Camera unavailable — exit cockpit first": "相机不可用——请先退出座舱", + "No shots to run": "没有可运行的镜头", + "Project exported": "项目已导出", + "Could not exit {mode} — scene layers may be refused": "无法退出 {mode}——场景图层可能被拒绝", + "Running {index}/{total}: {scene} / {shot}": "正在运行 {index}/{total}:{scene} / {shot}", + "Scene run complete": "场景运行完成", + "Global Flights Radar": "全球航班雷达", + "Orbital Watch": "轨道监视", + "Thermal Threat Board": "热源威胁看板", + "City Overload": "城市过载", + "Omniscience Pullback": "全知拉远", + "Shot {n}": "镜头 {n}", + "Density": "密度", + "Bloom": "泛光", + "VISUAL PRESETS": "视觉预设", + "LOCATION": "位置", + "CONTACTS — nearest planes · vessels · sites": "目标:最近的飞机 · 船舶 · 站点", + "SPACE MISSIONS — launches & orbital assets": "太空任务:发射与在轨资产", + "LIVE SIGNALS": "实时信号", + "CCTV layer unavailable": "CCTV 图层不可用", + "Link copied!": "链接已复制!", + "Copy failed": "复制失败", + "CCTV calibration saved": "CCTV 校准已保存", + "CCTV calibration reset": "CCTV 校准已重置", + "ADJUST ON": "调整开", + "ADJUST": "调整", + "UNKNOWN": "未知", + "ERROR": "错误", + "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 以激活" + }, + "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.": "停止自动翻页。上一页、下一页以及 SIG/NEWS/LOCAL 标签仍可使用。", + "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}", + "CLOUD {pct}%": "云量 {pct}%", + "CLOUD UNKNOWN": "云量未知", + "{source} · LOCATION QUERY": "{source} · 位置查询", + "REGIONAL NEWS": "地区新闻", + "CONTEXT STANDBY": "上下文待机", + "ENABLE GLOBAL CONTEXT FOR PROXIMITY PINGS": "启用全局上下文以获取邻近 PING", + "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": "可用输入均为最新 · 不代表安全无虞", + "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": "黑色电影", + "Snow": "雪景", + "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": "观测 / 地图标注 PING", + "SOURCE-BACKED EVENTS · NO SYNTHETIC NEWS": "来源可查的事件 · 无合成新闻", + "LATEST LOCATION-MATCHED REPORTING": "最新的位置匹配报道", + "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": "无法清除已选数据图层", + "Contacts could not complete the requested transition; try again": "目标未能完成所请求的切换,请重试", + "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": "上下文", + "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 活跃火点", + "Transit": "交通", + "ENABLING": "启用中", + "DISABLING": "停用中", + "Weather": "天气", + "{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": "已接受 {accepted} / {raw} 条记录", + "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": "不确定", + "{name}: {state}": "{name}:{state}", + "{count}s ago": "{count} 秒前", + "CONNECTING": "连接中", + "📍 Location: --": "📍 位置:--", + "Landmark: --": "地标:--", + "Searched location": "已搜索的位置", + "ORBIT": "环绕", + "Location not found": "未找到位置", + "Search failed": "搜索失败", + "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": "地图", + "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": "面板布局已更新——位置已重置为新的默认值", + "Collapse Cockpit display options": "收起座舱显示选项", + "Expand Cockpit display options": "展开座舱显示选项", + "Collapse Cockpit Radio controls": "收起座舱电台控件", + "Expand Cockpit Radio controls": "展开座舱电台控件", + "NO STATIONS": "无电台", + "NO STATION AVAILABLE": "没有可用电台", + "{name}, station {index} of {total}": "{name},第 {index} 个电台,共 {total} 个", + "No station available": "没有可用电台", + "OFF AIR": "停播", + "STATION UNAVAILABLE": "电台不可用", + "Radio could not {action} cleanly": "电台未能正常{action}", + "Close compact Radio controls": "关闭精简电台控件", + "Open compact Radio controls": "打开精简电台控件", + "SYNC": "同步", + "RECONCILE": "核对", + "DISABLE": "停用", + "ENABLE": "启用", + "Reconcile Radio — lifecycle uncertain": "核对电台——生命周期状态不确定", + "Disable Radio": "停用电台", + "Enable Radio": "启用电台", + "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": "{action}{target}电台", + "RADIO STATE UNCERTAIN": "电台状态不确定", + "SYNCING DIRECTORY": "正在同步目录", + "RADIO READY": "电台就绪", + "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": "电台生命周期状态不确定——请使用“启用”或“停用”进行核对", + "Loading": "加载中", + "unknown": "未知", + "Scene action failed": "场景操作失败", + "Delete shot \"{shot}\"?": "删除镜头“{shot}”?", + "SPARSE": "稀疏", + "BALANCED": "均衡", + "DEL": "删除", + "entity": "实体", + "Shared {subject} follow expired": "共享的 {subject} 跟随已过期", + "Shared {subject} could not be restored — feed unavailable": "无法恢复共享的 {subject}——数据源不可用", + "Shared {subject} is unavailable": "共享的 {subject} 不可用", + "ACQUIRING": "获取中", + "SHARED {subject}": "共享 {subject}", + "LOADING LIVE DATA": "正在加载实时数据", + "LOAD COMPLETE": "加载完成", + "LOAD CANCELLED": "加载已取消", + "LOAD FAILED": "加载失败", + "LIVE DATA OFF": "实时数据已关闭", + "MAPPED SITES LOADED": "已加载地图站点", + "RETRYING MAPPED SITES": "正在重试地图站点", + "FETCHING MAPPED SITES": "正在获取地图站点", + "OVERPASS TEMPORARILY UNAVAILABLE": "Overpass 暂时不可用", + "OVERPASS RATE-LIMITED": "Overpass 已限流", + "OVERPASS TIMED OUT": "Overpass 超时", + "OVERPASS COULD NOT COMPLETE THE QUERY": "Overpass 无法完成查询", + "TURNING OFF LIVE DATA": "正在关闭实时数据", + "REFRESHING LIVE DATA": "正在刷新实时数据", + "syncing road network": "正在同步路网", + "retry pending": "等待重试", + "retrying in {seconds}s": "{seconds} 秒后重试", + "Celestial ring — reveal the full globe": "天球环——显示完整地球", + "Celestial ring — available in Normal style": "天球环——仅在常规样式下可用", + "Detection overlay: {mode}": "探测叠加层:{mode}", + "Detection overlay: off": "探测叠加层:关闭", + "DETECT": "探测", + "AI AGENT": "AI 代理", + "Voice model tier — applies next session": "语音模型档位——下次会话生效", + "Estimated session cost": "预估会话费用", + "Voice control — activate to toggle voice; hold Space to speak": "语音控制——激活可切换语音;按住空格键说话", + "VOICE STANDBY": "语音待机", + "VOICE CONTROL": "语音控制", + "Hold Space to speak · tap Space to activate focused controls": "按住空格键说话 · 轻按空格键激活当前聚焦的控件", + "VOICE SYSTEM ERROR": "语音系统错误", + "DISMISS": "关闭", + "Check microphone permission and network access, then try again.": "请检查麦克风权限和网络连接,然后重试。", + "WebRTC microphone support unavailable": "不支持 WebRTC 麦克风", + "Requesting microphone": "正在请求麦克风", + "Release Space to send": "松开空格键发送", + "Hold Space to talk": "按住空格键说话", + "Ask or command": "提问或下达指令", + "Voice off": "语音已关闭", + "VOICE UNAVAILABLE": "语音不可用", + "VOICE ACTIVE": "语音已启用", + "Voice session could not be started.": "无法启动语音会话。", + "Next session: {pendingId} — this session stays on {modelId}": "下次会话:{pendingId}——本次会话仍使用 {modelId}", + "Voice model: {pendingId} — click to switch to {target}; applies next session": "语音模型:{pendingId}——点击切换到 {target};下次会话生效", + "Estimated session cost on {modelId} — {responses} response(s). Warns at {warn}, ends the session at {cap}.": "{modelId} 的预估会话费用——{responses} 次响应。达到 {warn} 时警告,达到 {cap} 时结束会话。", + "{tier} applies next session": "{tier} 将在下次会话生效", + "Session ended — cost cap {cost}": "会话已结束——已达费用上限 {cost}", + "Radio did not start": "电台未能启动", + "Running command": "正在执行指令", + "{items} {scope}": "{items} {scope}", + "degraded": "降级", + "stale": "过期", + "partial": "部分", + "fallback": "备用", + "unavailable": "不可用", + "Voice active": "语音已启用", + "Aircraft cockpit view": "飞机座舱视图", + "LEVEL": "等级", + "Estimated destination direction": "预估目的地方向", + "DEST ---°": "目的地 ---°", + "OPTICAL PLANE · 01": "光学平面 · 01", + "VISOR LOCK · ACTIVE": "面罩锁定 · 激活", + "GROUND SPEED · KTS": "地速 · KTS", + "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": "地速", + "ALTITUDE": "高度", + "Contact cockpit summary": "目标座舱摘要", + "CONTACT": "目标", + "CONTACTS · 250 KM": "目标 · 250 KM", + "Contact navigation": "目标导航", + "CONTEXT ONLY": "仅供参考", + "Nearby cohort counts": "附近各类目标数量", + "NEAREST OBSERVED / MAPPED": "最近的观测 / 地图标注目标", + "AVAILABLE INPUTS ONLY · NOT AN ALL-CLEAR": "仅限可用输入 · 不代表安全无虞", + "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": "降水", + "Show Live Signals": "显示实时信号", + "NEWS": "新闻", + "Show Regional News": "显示地区新闻", + "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": "座舱电台音量", + "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.": "应用高对比度的黑白黑色电影调色。", + "Add a cold, snowy whiteout treatment to the scene.": "为场景添加寒冷的雪天白茫茫效果。", + "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": "天气", + "CONTEXT": "上下文", + "Compact Radio controls": "精简电台控件", + "Open detailed Radio controls": "打开详细电台控件", + "Previous station": "上一个电台", + "Next station": "下一个电台", + "Compact Radio volume": "精简电台音量", + "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 / 空格键选择", + "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": "拖动调谐", + "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": "电台网站", + "Audio connects directly to the broadcaster after you press play. Your IP is visible to that broadcaster.": "点击播放后,音频将直接连接到广播方。广播方可以看到你的 IP。", + "Intelligence HUD (H)": "情报 HUD(H)", + "Layout": "布局", + "HUD layout": "HUD 布局", + "Tactical": "战术", + "Operator": "操作员", + "Minimal": "精简", + "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": "视域边缘羽化,以锁孔半径的百分比表示", + "Clear": "晴", + "Import": "导入", + "Celestial": "天球", + "Hide UI chrome": "隐藏界面控件", + "Clean UI": "纯净界面", + "Bloom / Glow": "泛光 / 辉光", + "Bloom intensity": "泛光强度", + "Sharpening": "锐化", + "Sharpen": "锐化", + "Sharpen intensity": "锐化强度", + "EXIT CLEAN VIEW": "退出纯净视图", + "Return UI controls": "恢复界面控件", + "Initializing photorealistic world...": "正在初始化逼真世界…", + "DATA LAYERS": "数据图层", + "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": "保存校准", + "RESET CAL": "重置校准", + "SCENE SUMMARY": "场景摘要", + "ALL": "全部", + "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": "复制分享链接", + "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.": "提示:停靠栏中的 GEV MIC 按钮可让你用语音操控地图。" +} From db16d8ed4cbb35717d9d971b203073bfd9b5a3c0 Mon Sep 17 00:00:00 2001 From: EDIT-Aeterna <73235923+EDIT-Aeterna@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:52:34 +0400 Subject: [PATCH 14/25] i18n(zh-CN): Simplified Chinese strings from #979 The Simplified Chinese localization by EDIT-Aeterna (#979), converted to the English-keyed catalog: 823 more entries for UI text that #914 did not cover. --- src/i18n/locales/zh-CN.json | 861 ++++++++++++++++++++++++++++++++++++ 1 file changed, 861 insertions(+) diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index cbedad420..3a5972058 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -1,4 +1,18 @@ { + "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.": "这条线没有长度——请在更远处点击。", + "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} 正在使用指针——请先关闭它。", "Flying to Austin, TX...": "正在飞往德克萨斯州奥斯汀…", "Restoring shared view...": "正在恢复共享视图…", "Configuring viewer...": "正在配置视图…", @@ -7,11 +21,86 @@ "Google 3D Tiles unavailable ({detail}). Loading the keyless globe...": "Google 3D Tiles 不可用({detail})。正在加载免密钥地球…", "Initializing systems...": "正在初始化系统…", "Close data attribution": "关闭数据来源说明", + "Light aircraft": "轻型飞机", + "Glider": "滑翔机", + "Turboprop": "涡桨飞机", + "Airliner": "喷气客机", + "Widebody": "宽体机", + "Four-engine jet": "四发喷气机", + "Helicopter": "直升机", + "Fast jet": "喷气战机", + "Business jet": "公务机", + "Drone": "无人机", "UNAVAILABLE": "不可用", "SOURCE": "来源", + "EVIDENCE": "证据", + "{count} GEOLOCATED SOURCE-MAP PLACEMENTS": { + "other": "{count} 个地理定位来源点位" + }, + "NO PREVIEW": "无预览", + "SOURCE LINK": "来源链接", + "{publisher} / OPEN": "{publisher} / 打开", + "Bhote Koshi flood reconstruction controls": "Bhote Koshi 洪水重构控制", + "EVENT RECONSTRUCTION · 26 AUG 2026": "事件重构 · 2026-08-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}", + "↗ 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": "历史参考 {before}%,灾后 {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": "载入下一个场景镜头", + "Rasuwa · 26 Aug 2026": "Rasuwa · 2026-08-26", + "Event reconstruction": "事件重构", "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…": "正在获取地图站点…", @@ -27,6 +116,9 @@ "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": "导航", @@ -56,6 +148,10 @@ "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": "活跃事件", @@ -74,6 +170,16 @@ "ANIME": "动漫", "NOIR": "黑色电影", "SNOW": "雪景", + "ARCTIC": "北极", + "ANTARCTIC": "南极洲", + "NORTH AMERICA": "北美洲", + "SOUTH AMERICA": "南美洲", + "EUROPE": "欧洲", + "AFRICA": "非洲", + "ASIA": "亚洲", + "OCEANIA": "大洋洲", + "NORTHERN OCEANIC GRID": "北方海洋网格", + "SOUTHERN OCEANIC GRID": "南方海洋网格", "LOADING": "加载中", "DEGRADED": "降级", "STALE": "过期", @@ -84,27 +190,77 @@ "Awaiting telemetry...": "等待遥测数据…", "REC": "REC", "AIS: --": "AIS: --", + "SECTOR {lat} {lon}": "扇区 {lat} {lon}", "POWER UP · {count} KEYS WAITING": { "other": "启动 · {count} 个密钥待配置" }, "POWERED UP": "已启动", + "Invalid JSON": "无效的 JSON", "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": "按量计费——需要启用结算的账号", + "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}", + "Project keys: {status}": "项目密钥:{status}", "your app configuration": "你的应用配置", "your local .env": "你的本地 .env", "Saving…": "正在保存…", "Save failed: {detail}": "保存失败:{detail}", "Paste at least one key first.": "请先至少粘贴一个密钥。", "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}中移除。正在重启——本页面将自动重新加载。", "Places context + Street View fallback; optional separate key": "地点上下文 + 街景备用;可选的独立密钥", + "Voice control — API key or ChatGPT OAuth": "语音控制——API 密钥或 ChatGPT OAuth", "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", + "Low": "低", + "High": "高", + "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.": "选中后青色摄像头变为珊瑚色。扇形仅示意已标绘方向,并非实测覆盖范围。附近的摄像头可能不在屏幕内。", + "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 摄像头", + "At least {count}": "至少 {count}", "CONTEXT READY": "上下文就绪", "GLOBAL CONTEXT OFF": "全局上下文已关闭", "SELECT A FLIGHT, VESSEL, OR MAPPED INSTALLATION": "选择一个航班、船舶或地图标注设施", @@ -117,6 +273,7 @@ "FOCUS": "聚焦", "Next — nearest unvisited contact in the 250 km window": "下一个:250 km 范围内最近的未访问目标", "NEXT": "下一个", + "Named areas ({n})": "指定区域({n})", "Open-source mapped/observed context. Missing broadcasts, unloaded map areas, or unmapped sites are not evidence of absence.": "开源的地图标注/观测上下文。缺失的广播、未加载的地图区域或未标注的站点不能作为不存在的证据。", "HDG {value}°": "HDG {value}°", "BRG {value}°": "BRG {value}°", @@ -150,8 +307,83 @@ "CAL {value}": "CAL {value}", "SRC {value}": "SRC {value}", "{value} CONTEXT": "{value} 上下文", + "Atlantic and eastern/central North Pacific; not worldwide cyclone coverage.": "覆盖大西洋及北太平洋东部/中部;不含全球气旋。", + "Cyclone advisories unavailable": "气旋公告不可用", + "Potential 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": "大西洋 · 东/中太平洋", + "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": "骑行", + "{error} — try again in a moment": "{error} — 请稍后重试", + "Routing is rate limited — try again in a moment": "路线请求过于频繁 — 请稍后重试", + "Routing unavailable (HTTP {status})": "路线服务不可用(HTTP {status})", + "Routing unavailable": "路线规划不可用", + "Head out on {road}": "沿{road}出发", + "Head out": "出发", + "Arrive at your destination": "到达目的地", + "{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", + "Remove the route and both markers": "移除路线和两个标记点", + "press Escape twice to leave Draw": "按两次 Escape 退出绘图", + "{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}": "{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}": "第 {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": "传感器 n/a", @@ -166,12 +398,15 @@ "<1m ago": "不到 1 分钟前", "{count}m ago": "{count} 分钟前", "{count}h ago": "{count} 小时前", + "Fire · FRP {frp} MW": "火点 · FRP {frp} MW", "STALE · cached {age}": "过期 · 缓存于 {age}", "refreshing...": "刷新中…", "loading...": "加载中…", "KEY REQUIRED": "需要密钥", "LIVE · updated {age}": "实时 · 更新于 {age}", "loading mapped installation context": "正在加载地图标注设施上下文", + "Too many mapped sites in view to list them all": "视野内标绘地点过多,无法全部列出", + "Installation context unavailable": "设施背景信息不可用", "MAPPED INSTALLATION": "地图标注设施", "Unknown": "未知", "LAUNCH SITE · {site}": "发射场 · {site}", @@ -240,11 +475,50 @@ "Ascent replay": "上升回放", "Orbit replay": "轨道回放", ", paused": ",已暂停", + "Heard by your receiver": "由你的接收机接收", + "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": "无呼号", + "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": "运营方未知", + "{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": "未控制或未知", + "Under 50% contained": "控制低于 50%", + "50–99% contained": "控制 50–99%", + "Fully contained": "已全部控制", + "Colour shows reported containment. Perimeters are simplified to about 100 m.": "颜色表示报告的控制比例。周界简化至约 100 米。", "News": "新闻", "Talk": "谈话", "Weather / Emergency": "天气 / 应急", @@ -253,6 +527,10 @@ "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.": "电台目录暂时不可用。", "Alternative": "另类", "Ambient": "氛围", "Blues": "蓝调", @@ -287,7 +565,22 @@ "DENSE ···": "密集 ···", "DENSE ✕": "密集 ✕", "DENSE": "密集", + "DOCKED · {name}": "对接 · {name}", + "KEY REJECTED": "密钥被拒绝", + "loading coverage...": "正在加载覆盖范围...", "Selected": "所选", + "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": "详细覆盖范围受限", + "OpenStreetMap roads, with TomTom flow matched when available": "OpenStreetMap 道路,可用时匹配 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} · 视野内无流量道路", @@ -299,6 +592,7 @@ "SIMULATED · Roads: OpenStreetMap · Flow: TomTom (no matches)": "模拟 · 道路:OpenStreetMap · 流量:TomTom(无匹配)", "SIMULATED — traffic service unreachable": "模拟——交通服务无法连接", "SIMULATED — add TomTom key for live": "模拟——添加 TomTom 密钥以获取实时数据", + "Route {route}": "航线{route}", "AIS: {name}": "AIS: {name}", "{type} SPD: {speed} HDG: {heading}": "{type} SPD: {speed} HDG: {heading}", "VESSEL": "船舶", @@ -307,39 +601,222 @@ "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 数据", + "Hidden by this map source · choose a globe map": "已被当前地图源隐藏 · 请选择球体地图", + "Frame unavailable; previous observation retained": "帧不可用;保留上一帧观测", + "Weather imagery unavailable": "天气影像不可用", + "Weather source unavailable; previous observation retained": "天气数据源不可用;保留上一帧观测", + "Weather unavailable": "天气数据不可用", + "{hours}h {minutes}m ago": "{hours} 小时 {minutes} 分前", "{minutes}m ago": "{minutes} 分钟前", "{state} · {time} · {lag}": "{state} · {time} · {lag}", + "Observed": "观测", + "History": "历史", + "Observation unavailable": "观测不可用", + "Waiting for observation": "等待观测数据", + "RADAR REFLECTIVITY · dBZ": "雷达反射率 · dBZ", + "LIGHTNING DENSITY · 15 min accumulation": "闪电密度 · 15 分钟累计", + "GLOBAL INFRARED · hourly": "全球红外 · 每小时", + "GOES INFRARED · ~5 min": "GOES 红外 · 约 5 分钟", "{state}: {time}": "{state}:{time}", + "Latest observation": "最新观测", "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": "地图中心在覆盖范围之外", + "N. America": "北美", + "Global": "全球", + "Hourly global mosaic; usually 2–3 hours delayed": "每小时全球拼图;通常延迟 2–3 小时", + "GOES regional clouds; approximately 5-minute updates": "GOES 区域云图;约 5 分钟更新", + "Clouds only": "仅云区", + "Full": "完整", + "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.": "NOAA/NWS 15 分钟闪电密度,来源为 Vaisala NLDN/GLD360。覆盖自 110°E 经太平洋/美洲至 0°,25°S–80°N。不是实时闪击计数、不是全球覆盖,也不构成安全警示。", + "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 公里分辨率;显示限制在第 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": "图像", + "OPACITY": "不透明度", + "Weather tiles unavailable · previous frame retained": "天气瓦片不可用 · 保留上一帧", + "Some weather tiles unavailable": "部分天气瓦片不可用", + "Temperature field unavailable": "温度字段不可用", "Wind": "风", + "Wind source unavailable": "风场数据源不可用", + "Forecast · does not follow history": "预报 · 不随观测历史变化", + "Wind motion": "风场运动", + "Wind speed": "风速", + "Sea-level pressure": "海平面气压", + "Air temperature · 2 m": "2 米气温", + "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": "地面以上两米的气温", + "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}", + "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 米高度的地表风。约 1° 全球网格。流线沿 10 米风场绘制,为便于观看抬升至 12 公里;显示高度并非天气高度。视图光照仅为可读性服务。动画展示的是单一固定预报内的流场,不推进时间。颜色字段贴附于球体底图或当前的照片级 3D Tiles。", + "MODEL": "模型", + "FIELD": "字段", + "UNITS": "单位", + "MOTION": "运动", + "Forecast": "预报", + "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° 网格插值的模式预报。反映大范围天气形势,不是街道级实测。", + " from {direction}": "从{direction}出发", + "WIND AT {coordinates}": "{coordinates} 的风", + "{model} · valid {time}": "{model} · 生效 {time}", + "Globe imagery unavailable": "球体影像不可用", + "Globe field image unavailable": "球体字段图像不可用", "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 瓦片不可用 — 请检查密钥的 API 限制、配额或网络", "Visible map targets": "可见的地图目标", "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": "全知拉远", + "Scene {n}": "场景 {n}", "Shot {n}": "镜头 {n}", + "Could not complete this action. Check the file, references and selected assets.": "无法完成此操作。请检查文件、引用和所选资源。", + "{scene} / {pack}: {status}. File: {path}. {attribution} · {license}": "{scene} / {pack}:{status}。文件:{path}。{attribution} · {license}", + "Unavailable layers: {layers}": "不可用图层:{layers}", + "{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": "资源包已下载", "Density": "密度", + "Gain": "增益", "Bloom": "泛光", "VISUAL PRESETS": "视觉预设", "LOCATION": "位置", @@ -349,12 +826,18 @@ "CCTV layer unavailable": "CCTV 图层不可用", "Link copied!": "链接已复制!", "Copy failed": "复制失败", + "Tilt map to oblique view": "将地图倾斜至斜视角", "CCTV calibration saved": "CCTV 校准已保存", + "RANGE": "范围", + "{value}m": "{value}分钟", "CCTV calibration reset": "CCTV 校准已重置", "ADJUST ON": "调整开", "ADJUST": "调整", + "LIVE": "实时", "UNKNOWN": "未知", "ERROR": "错误", + "Configured source": "已配置数据源", + "Configured Source": "已配置数据源", "SOURCE · UNKNOWN": "来源 · 未知", "FRAME · LOADING": "画面 · 加载中", "FRAME · UNAVAILABLE": "画面 · 不可用", @@ -504,6 +987,11 @@ "{mode} could not start because another layer did not stop cleanly": "{mode}无法启动,因为另一个图层未能正常停止", "Space Missions": "太空任务", "Context": "上下文", + "Rendered globe frames per second · toggle with `": "每秒渲染的地球帧数 · 按 ` 键切换", + "Show {name}": "显示 {name}", + "Fly to {name}": "飞往{name}", + "Press on the ground, not the sky": "请按在地面,而不是天空", + "Drag to size the box": "拖动以调整框选大小", "Live Flights": "实时航班", "Military Flights": "军用航班", "Earthquakes (24h)": "地震(24 小时)", @@ -520,10 +1008,32 @@ "Dams": "水坝", "Submarine Cables": "海底电缆", "FIRMS Active Fires": "FIRMS 活跃火点", + "Local ADS-B": "本地 ADS-B", "Transit": "交通", + "Recent Imagery": "近期影像", + "Fire Perimeters": "火场周界", + "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": "军用飞机", + "GFS / ECMWF IFS · FORECAST": "GFS / ECMWF IFS · 预报", + "NOAA nowCOAST · OBSERVED": "NOAA nowCOAST · 观测", "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} 秒后重试", @@ -537,15 +1047,39 @@ "{state} · {source} · {ago}": "{state} · {source} · {ago}", "{source} · {ago}": "{source} · {ago}", "UNCERTAIN": "不确定", + "Data source unavailable": "数据源不可用", "{name}: {state}": "{name}:{state}", "{count}s ago": "{count} 秒前", "CONNECTING": "连接中", + "TUNING": "调谐中", + "STREAMING": "正在流式接收", + "waiting for IQ": "等待 IQ 数据", + "{count} messages": { + "other": "{count} 条消息" + }, + "DSP {count} blocks": { + "other": "DSP {count} 个模块" + }, + "DSP waiting": "DSP 等待中", + "audio signal --": "音频信号 --", + "audio {state}": "音频 {state}", + "{count} HEARD": { + "other": "已收到 {count} 架" + }, + "DISCONNECT": "断开连接", + "CONNECT": "连接", + "LOCATING…": "定位中…", + "LOCATED": "已定位", + "LOCATE": "定位", + " · read while Local ADS-B is on": " · 仅在本地 ADS-B 开启时读取", + "Interface language": "界面语言", "📍 Location: --": "📍 位置:--", "Landmark: --": "地标:--", "Searched location": "已搜索的位置", "ORBIT": "环绕", "Location not found": "未找到位置", "Search failed": "搜索失败", + "Detailed outline unavailable": "无法获取详细轮廓", "Fly to a POI first": "请先飞往一个 POI", "Reset to full globe view": "重置为完整地球视图", "Reset cockpit to full globe view": "将座舱重置为完整地球视图", @@ -560,17 +1094,30 @@ "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": "华盛顿特区", + "Tallinn": "塔林", "Collapse Cockpit display options": "收起座舱显示选项", "Expand Cockpit display options": "展开座舱显示选项", "Collapse Cockpit Radio controls": "收起座舱电台控件", "Expand Cockpit Radio controls": "展开座舱电台控件", + "CH {index} / {total}": "频道 {index} / {total}", "NO STATIONS": "无电台", "NO STATION AVAILABLE": "没有可用电台", "{name}, station {index} of {total}": "{name},第 {index} 个电台,共 {total} 个", "No station available": "没有可用电台", "OFF AIR": "停播", "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": "同步", @@ -619,19 +1166,119 @@ "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", + "{owner} is using the pointer — close it first": "{owner} 正在使用指针——请先将其关闭", + "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": "加载中", "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}", + "preview": "预览", + "start here": "从此开始", + "No box": "无框选", + "Searching": "搜索中", + "Every day is empty here": "此处每天都为空", + "Sources off": "数据源未开启", + "Not set": "未设置", + "Basemap": "底图", + "Unpin {slot}": "取消钉住 {slot}", + "Unpin the image": "取消钉住图像", + "EXPORT {slot}": "导出 {slot}", + "EXPORT": "导出", + "Download {slot} as a PNG": "将 {slot} 下载为 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": "删除", + "Cancel": "取消", + "EDIT DETAILS": "编辑详情", + "SHARE SCENE": "分享场景", "entity": "实体", "Shared {subject} follow expired": "共享的 {subject} 跟随已过期", "Shared {subject} could not be restored — feed unavailable": "无法恢复共享的 {subject}——数据源不可用", "Shared {subject} is unavailable": "共享的 {subject} 不可用", "ACQUIRING": "获取中", "SHARED {subject}": "共享 {subject}", + "subject": "目标", "LOADING LIVE DATA": "正在加载实时数据", "LOAD COMPLETE": "加载完成", "LOAD CANCELLED": "加载已取消", @@ -640,27 +1287,86 @@ "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 摄像头", + "Loading {feeds}": "正在加载 {feeds}", + "{count} heard": { + "other": "已收到 {count} 架" + }, + "{rate} msg/s": "{rate} msg/s", + "Street Level could not start": "街景未能启动", + "Street Level toggle failed": "街景切换失败", + "Street-level image": "街景图像", + "SHRINK": "缩小", + "EXPAND": "展开", + "Shrink": "缩小", + "Expand": "展开", + "ANY DATE": "任意日期", + "LAST 10 YEARS": "最近 10 年", + "LAST 5 YEARS": "最近 5 年", + "LAST 3 YEARS": "最近 3 年", + "LAST 2 YEARS": "最近 2 年", + "LAST YEAR": "最近一年", + "LAST 6 MONTHS": "最近 6 个月", + "LAST 3 MONTHS": "最近 3 个月", + "LAST MONTH": "最近一个月", + "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}", + "Camera follows view: move the globe camera wherever the street-level view looks": "相机跟随视野:街景看向哪里,地球相机就移到哪里", + "Camera follow needs the Google 3D map: choose Google 3D under MAP SOURCE": "相机跟随需要 Google 3D 地图:请在地图源(MAP SOURCE)中选择 Google 3D", + "Switch a provider on to draw its coverage.": "开启一个提供商以绘制其覆盖范围。", + "Loading this sequence…": "正在加载此序列…", + "{n} images in this sequence · Esc clears": { + "other": "此序列共 {n} 张图像 · Esc 清除" + }, + "{n} sequences in view · click a line for its photos": { + "other": "视野内共 {n} 条序列 · 点击线段查看其照片" + }, "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}", + "Forecast · valid {valid}": "预报 · 生效 {valid}", "AI AGENT": "AI 代理", "Voice model tier — applies next session": "语音模型档位——下次会话生效", "Estimated session cost": "预估会话费用", "Voice control — activate to toggle voice; hold Space to speak": "语音控制——激活可切换语音;按住空格键说话", + "ON/OFF": "开/关", "VOICE STANDBY": "语音待机", "VOICE CONTROL": "语音控制", "Hold Space to speak · tap Space to activate focused controls": "按住空格键说话 · 轻按空格键激活当前聚焦的控件", + "Voice assistant": "语音助手", + "VOICE": "语音", + "Dismiss voice card": "关闭语音卡片", + "YOU": "用户", + "THIS": "此目标", + "Plan": "计划", + "Referenced items": "引用条目", + "NOTES & SOURCES": "笔记与来源", "VOICE SYSTEM ERROR": "语音系统错误", "DISMISS": "关闭", "Check microphone permission and network access, then try again.": "请检查麦克风权限和网络连接,然后重试。", @@ -675,18 +1381,89 @@ "Voice session could not be started.": "无法启动语音会话。", "Next session: {pendingId} — this session stays on {modelId}": "下次会话:{pendingId}——本次会话仍使用 {modelId}", "Voice model: {pendingId} — click to switch to {target}; applies next session": "语音模型:{pendingId}——点击切换到 {target};下次会话生效", + "COST UNKNOWN": "费用未知", + "ChatGPT OAuth session: {responses} response(s), {input} input and {output} output tokens, {captions} caption transcription(s) reported.": "ChatGPT OAuth 会话:已报告 {responses} 个响应、{input} 个输入 token、{output} 个输出 token、{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.": "估算不完整 — 会话结束时仍有响应在处理中,因此其用量从未上报。", "{tier} applies next session": "{tier} 将在下次会话生效", "Session ended — cost cap {cost}": "会话已结束——已达费用上限 {cost}", "Radio did not start": "电台未能启动", "Running command": "正在执行指令", + "{n} fires": { + "other": "{n} 处火点" + }, + "anywhere in the loaded data": "已加载数据中的任意位置", + "in view": "视野内", + "over {region}": "{region} 范围内", + "At least {items}": "至少 {items}", "{items} {scope}": "{items} {scope}", + "{label} off": "{label} 已关闭", + "counted the first {returned} loaded records": "已统计最先加载的 {returned} 条记录", + "counted {returned} of {total} loaded records": "已统计已加载 {total} 条记录中的 {returned} 条", + "Fly below {km} km to a covered region": "请飞行至 {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.": "无法启动语音。", + "{value} feet": "{value} 英尺", + "nominal": "正常", + "{label} on": "{label} 已开启", + "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}": "{distance} 自 {location}", + "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 {target}": "取景{target}", + "Mark the map": "在地图上标注", + "Fly to {place}": "飞往{place}", + "Nearest aircraft to {place}": "距离{place}最近的飞机", + "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": "完成", + "HERE": "此处", "Aircraft cockpit view": "飞机座舱视图", "LEVEL": "等级", "Estimated destination direction": "预估目的地方向", @@ -731,9 +1508,13 @@ "WIND": "风", "SKY": "天空", "PRECIP": "降水", + "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": "显示", @@ -745,6 +1526,8 @@ "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": "重置", @@ -772,6 +1555,9 @@ "Search location by name or coordinates": "按名称或坐标搜索位置", "Search any location...": "搜索任意位置…", "WEATHER": "天气", + "Active weather products": "活跃天气产品", + "RECENT IMAGERY": "近期影像", + "Imagery days in the box": "影像盒内的天数", "CONTEXT": "上下文", "Compact Radio controls": "精简电台控件", "Open detailed Radio controls": "打开详细电台控件", @@ -801,6 +1587,7 @@ "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": "电台播放", @@ -811,13 +1598,48 @@ "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 图层共享。", + "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": "消息/秒", + "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": "战术", "Operator": "操作员", "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": "探测标签密度", @@ -841,7 +1663,22 @@ "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": "清除图板上的所有标记", "Import": "导入", "Celestial": "天球", "Hide UI chrome": "隐藏界面控件", @@ -870,7 +1707,28 @@ "SAVE CAL": "保存校准", "RESET CAL": "重置校准", "SCENE SUMMARY": "场景摘要", + "STREET LEVEL": "街景", + "Expand the street-level view (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": "填充:图像铺满画框并裁剪照片", + "Camera follows view": "相机跟随视野", + "FOLLOW": "跟随", + "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": "时间范围", + "Coverage colours": "覆盖图例", "SCENES": "场景", "Scene recipe": "场景方案", "NEW": "新建", @@ -893,6 +1751,9 @@ "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": "选择你的首个视图", From c4cc499d581650a5d3a95c7f08cb9b54db575e49 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Wed, 21 Oct 2026 12:52:34 +0400 Subject: [PATCH 15/25] i18n(zh-CN): complete and ship Simplified Chinese MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Translate the remaining 910 strings and review the whole catalog (meaning, terms, punctuation, label length) against the screens. - Register zh-CN: any zh tag maps to it except Traditional ones (Hant, TW, HK, MO), which keep English until a Traditional catalog ships. - Hemisphere letters get their own context so the HUD sector line reads 21.33N, not a compass word; the Bhote Koshi imagery dates follow year-first languages (2021年10月16日). - cjk.css: tight tracking for Chinese text and a 9px floor on the dock voice labels and layer buttons; Latin HUD readouts keep their look. - docs/translations/zh-CN.md: style, plurals and glossary. Co-Authored-By: Claude Opus 5.5 --- docs/CURRENT-STATE.md | 6 +- docs/TRANSLATORS.md | 4 +- docs/translations/zh-CN.md | 59 ++ scripts/package-boundaries.json | 89 ++- src/data/bhoteKoshiEvent.js | 10 +- src/hudLocalityFormat.js | 13 +- src/i18n/i18n.test.mjs | 6 + src/i18n/index.js | 3 +- src/i18n/locale.js | 13 +- src/i18n/locales/es.json | 8 +- src/i18n/locales/ru.json | 8 +- src/i18n/locales/zh-CN.json | 1306 +++++++++++++++++++++++++++---- src/i18n/same-as-english.json | 58 ++ src/ui/localePicker.test.mjs | 8 +- src/ui/styles/cjk.css | 29 + style.css | 1 + 16 files changed, 1422 insertions(+), 199 deletions(-) create mode 100644 docs/translations/zh-CN.md create mode 100644 src/ui/styles/cjk.css diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 045d6e26b..68dcb858b 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -3369,7 +3369,7 @@ test:track` 43 tracking invariants · headless QA harnesses under ### Interface localization (October 2026) -Shipped locales: **en** (source and fallback), **es** and **ru**. The code lives in +Shipped locales: **en** (source and fallback), **es**, **ru** and **zh-CN**. The code lives in `src/i18n/`. How to mark UI text: [`docs/I18N.md`](./I18N.md); translator workflow: [`docs/TRANSLATORS.md`](./TRANSLATORS.md). @@ -3380,7 +3380,9 @@ Shipped locales: **en** (source and fallback), **es** and **ru**. The code lives value). English has no catalog; every other locale is `src/i18n/locales/.json`, and a missing entry falls back to English. - **Locale choice.** `?lang=` → stored choice (`gev:locale:v1`) → - `navigator.languages` → English. `?lang=` is not stored, and share links drop + `navigator.languages` → English. Any `zh` tag maps to `zh-CN` except + Traditional ones (`Hant`, `TW`, `HK`, `MO`), which fall through until a + Traditional catalog ships. `?lang=` is not stored, and share links drop it, so a recipient keeps their own language. The globe picker in the bottom-right corner (`src/ui/localePicker.js`) stores the choice, drops `?lang=` (keeps it only when storage is blocked) and reloads. It asks first diff --git a/docs/TRANSLATORS.md b/docs/TRANSLATORS.md index 2131ab7a3..538f8c50e 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`) and **Russian** (`ru`). +Shipped languages: **English** (source and fallback), **Spanish** (`es`), **Russian** (`ru`) and **Simplified Chinese** (`zh-CN`). ## 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), [Russian](./translations/ru.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`: [Spanish](./translations/es.md), [Russian](./translations/ru.md), [Simplified Chinese](./translations/zh-CN.md). Read it before translating, and add a term there when you settle on one. diff --git a/docs/translations/zh-CN.md b/docs/translations/zh-CN.md new file mode 100644 index 000000000..9b779e0b7 --- /dev/null +++ b/docs/translations/zh-CN.md @@ -0,0 +1,59 @@ +# Simplified Chinese (`zh-CN`) + +Catalog: `src/i18n/locales/zh-CN.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md). + +The catalog started from the Chinese locale by abiyeenzo (#914) and the +Simplified Chinese localization by EDIT-Aeterna (#979); the rest was translated +for the current English and reviewed against both. + +## Style + +- Mainland usage and Simplified characters. Address the user neutrally, without 您/你 where a plain imperative works: 「点击地图」, 「重试」. +- Chinese has no case: an UPPERCASE English HUD label becomes the same short Chinese label. Keep labels as short as the English: 「实时」, 「过期」, 「关」. +- Full-width punctuation in Chinese text: ,。:;?!()“”. A dash is 「——」 without spaces; keep `·` as the HUD separator and `…` for ellipsis. +- One space between Chinese and Latin letters or digits: 「AIS 船舶」, 「24 小时」, 「{count} 个」. No space next to full-width punctuation. +- Units: km, m, km/h, m/s, kn, ft, MW, °, % stay as written; time words are 小时, 分钟, 秒 after a number (「5 分钟前」). +- Product names, provider names and codes stay as they are: Google, Cesium, CelesTrak, NOAA, FIRMS, AIS, ADS-B, GNSS, CCTV, HUD, CRT, NVG, FLIR, WHOT/BHOT, Ironbow, CONUS. + +## Plurals + +`Intl.PluralRules('zh-CN')` has one category, `other`: a `tn()` entry has only that form. Use a measure word next to the number where Chinese needs one: 「{count} 个密钥」, 「{count} 架飞机」, 「{count} 艘船」. + +## Glossary + +| English | 中文 | +| ------------------------------------- | --------------------------- | +| layer / data layer | 图层 / 数据图层 | +| feed | 数据源 | +| source / data source | 来源 / 数据源 | +| LIVE (status) / live data | 实时 / 实时数据 | +| SIMULATED | 模拟 | +| stale / degraded / unavailable | 过期 / 降级 / 不可用 | +| retry | 重试 | +| key (API key) / POWER UP | 密钥(API 密钥)/ 增强 | +| Provider Settings | 服务商设置 | +| globe / map / map source / basemap | 地球 / 地图 / 地图源 / 底图 | +| aircraft / flight / military flights | 飞机 / 航班 / 军用航班 | +| vessel, ship | 船舶 | +| satellite | 卫星 | +| launch (rocket) / space mission | 发射 / 太空任务 | +| contact (tracked object) | 目标 | +| installation, site | 设施 | +| camera / CCTV camera | 摄像头 / CCTV 摄像头 | +| earthquake / active fire | 地震 / 活跃火点 | +| scene / shot | 场景 / 镜头 | +| briefing / panel / dock | 简报 / 面板 / 停靠栏 | +| preset / visual preset | 预设 / 视觉预设 | +| Display / HUD layout | 显示 / HUD 布局 | +| Tactical / Operator / Minimal / Cyber | 战术 / 操作员 / 精简 / 赛博 | +| Scope (circular viewport mask) | 视域 | +| Clean UI | 纯净界面 | +| cockpit | 座舱 | +| heading / altitude / ground speed | 航向 / 高度 / 地速 | +| Street Level / Street View (Google) | 街景 / 街景(Google) | +| Recent Imagery | 近期影像 | +| Global Context / Context (panel) | 全局态势 / 态势 | +| radio (station) | 电台 | +| transit / bikeshare | 公共交通 / 共享单车 | +| Previous / Next | 上一个 / 下一个 | +| compass points (N, NNE, …) | 北, 北东北, … | diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 264f58440..bbbc39574 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -426,7 +426,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": [] }, @@ -451,7 +452,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": ["cesium"] }, @@ -467,7 +469,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": [] }, @@ -495,6 +498,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/unitLabels.js", "src/ui/keyGuidance.js", "src/ui/statusText.js" @@ -513,7 +517,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": [] }, @@ -528,7 +533,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": [] }, @@ -546,6 +552,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/unitLabels.js" ], "external": [] @@ -570,6 +577,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/layerNames.js" ], "external": [] @@ -603,6 +611,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/unitLabels.js" ], "external": ["cesium"] @@ -617,6 +626,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/unitLabels.js" ], "external": [] @@ -765,6 +775,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/unitLabels.js", "src/ui/keyGuidance.js", "src/ui/layerNames.js", @@ -882,6 +893,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/unitLabels.js" ], "external": ["cesium"] @@ -947,6 +959,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/unitLabels.js" ], "external": ["cesium"] @@ -992,7 +1005,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": ["cesium"] }, @@ -1052,7 +1066,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": ["cesium"] }, @@ -1081,6 +1096,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/unitLabels.js", "src/layers/recentImagery/display.js" ], @@ -1109,7 +1125,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": ["cesium"] }, @@ -1140,6 +1157,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/unitLabels.js" ], "external": ["cesium", "satellite.js"] @@ -1171,7 +1189,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": ["cesium"] }, @@ -1213,6 +1232,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/layers/installations/classLabel.js" ], "external": ["cesium", "@mapbox/vector-tile", "pbf"] @@ -1247,6 +1267,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/unitLabels.js" ], "external": ["cesium"] @@ -1295,7 +1316,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": ["cesium", "pbf", "@mapbox/vector-tile"] }, @@ -1323,7 +1345,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": ["cesium"] }, @@ -1370,7 +1393,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": ["hls.js", "cesium"] }, @@ -1435,7 +1459,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": ["cesium", "pbf", "@mapbox/vector-tile", "mapillary-js"] }, @@ -1462,7 +1487,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": ["cesium", "@mapbox/vector-tile", "pbf"] }, @@ -1501,7 +1527,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": ["cesium"] }, @@ -1522,7 +1549,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": ["cesium"] }, @@ -1577,7 +1605,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": ["cesium"] }, @@ -1633,6 +1662,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/statusText.js" ], "external": [] @@ -2486,6 +2516,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/unitLabels.js", "src/layers/recentImagery/display.js", "src/ui/keyGuidance.js", @@ -3059,6 +3090,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/unitLabels.js", "src/layers/recentImagery/display.js", "src/layers/installations/classLabel.js", @@ -3164,6 +3196,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/i18n/index.js" ], "external": ["cesium"] @@ -3184,7 +3217,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": [] }, @@ -3216,7 +3250,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": ["cesium"] }, @@ -3240,7 +3275,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": [] }, @@ -3331,6 +3367,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/unitLabels.js" ], "external": ["cesium"] @@ -3385,6 +3422,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/statusText.js" ], "external": [] @@ -3636,7 +3674,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": ["cesium", "egm96-universal", "pbf"] }, @@ -3678,7 +3717,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": ["cesium", "pbf"] }, @@ -3730,7 +3770,8 @@ "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/es.json", - "src/i18n/locales/ru.json" + "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json" ], "external": ["cesium"] }, @@ -3746,6 +3787,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/statusText.js", "src/ui/layerNames.js" ], @@ -3938,6 +3980,7 @@ "src/i18n/locale.js", "src/i18n/locales/es.json", "src/i18n/locales/ru.json", + "src/i18n/locales/zh-CN.json", "src/ui/statusText.js", "src/ui/layerNames.js" ], diff --git a/src/data/bhoteKoshiEvent.js b/src/data/bhoteKoshiEvent.js index 2baaf7031..434aff6d0 100644 --- a/src/data/bhoteKoshiEvent.js +++ b/src/data/bhoteKoshiEvent.js @@ -412,14 +412,18 @@ const IMAGERY_LABEL_NAMES = Object.freeze([ [/^(\d{4}) POST-EVENT$/, msg('{year} POST-EVENT')], ]); -/** Day, short month and year in the active language: 16 OCT 2021, 16 ОКТ. 2021. */ +/** Day, short month and year in the active language: 16 OCT 2021, 16 ОКТ. 2021, 2021年10月16日. */ function imageryDateText(observedAt) { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(observedAt || '')); if (!match) return ''; const date = new Date(Date.UTC(+match[1], +match[2] - 1, +match[3])); if (Number.isNaN(date.getTime())) return ''; const part = (options) => formatDate(date, { timeZone: 'UTC', ...options }); - return `${part({ day: 'numeric' })} ${part({ month: 'short' })} ${part({ year: 'numeric' })}`.toUpperCase(); + const year = part({ year: 'numeric' }); + // Year-first languages (2021年10月16日) keep their own order and units. + const whole = part({ day: 'numeric', month: 'short', year: 'numeric' }); + if (whole.startsWith(year)) return whole; + return `${part({ day: 'numeric' })} ${part({ month: 'short' })} ${year}`.toUpperCase(); } /** @@ -1029,7 +1033,7 @@ function createPanel(event, handlers) { ${escapeMarkup(t('GEOLOCATIONS · {credit}', { credit: 'GEO GEORGE SHADRACH' }))} -

${escapeMarkup(t('Clouds are preserved from the source imagery.'))}

+

${escapeMarkup(t(' Clouds are preserved from the source imagery.'))}

`; const refs = collectPanelReferences(panel); diff --git a/src/hudLocalityFormat.js b/src/hudLocalityFormat.js index 4a82fdbb3..84278564b 100644 --- a/src/hudLocalityFormat.js +++ b/src/hudLocalityFormat.js @@ -35,7 +35,16 @@ export const LOCALIZED_LOCALITY = Object.freeze({ }), sector: ({ latDeg, lonDeg }) => t('SECTOR {lat} {lon}', { - lat: hudCoordinate(latDeg, t('N'), t('S')), - lon: hudCoordinate(lonDeg, t('E'), t('W')), + // Hemisphere letters, not compass words: 21.33N, never 21.33北. + lat: hudCoordinate( + latDeg, + t('N', { context: 'hemisphere' }), + t('S', { context: 'hemisphere' }), + ), + lon: hudCoordinate( + lonDeg, + t('E', { context: 'hemisphere' }), + t('W', { context: 'hemisphere' }), + ), }), }); diff --git a/src/i18n/i18n.test.mjs b/src/i18n/i18n.test.mjs index a662130a1..70eaf7e86 100644 --- a/src/i18n/i18n.test.mjs +++ b/src/i18n/i18n.test.mjs @@ -35,6 +35,12 @@ test('normalizeLocale folds regional variants and rejects unshipped tags', () => assert.equal(normalizeLocale(' RU '), 'ru'); assert.equal(normalizeLocale('en_GB'), 'en'); assert.equal(normalizeLocale('xx'), null); + // Simplified Chinese by default; Traditional tags wait for zh-TW. + assert.equal(normalizeLocale('zh'), 'zh-CN'); + assert.equal(normalizeLocale('zh-CN'), 'zh-CN'); + assert.equal(normalizeLocale('zh-Hans-SG'), 'zh-CN'); + assert.equal(normalizeLocale('zh-TW'), null); + assert.equal(normalizeLocale('zh-Hant-HK'), null); assert.equal(normalizeLocale(undefined), null); }); diff --git a/src/i18n/index.js b/src/i18n/index.js index ccc54a5e6..d5698aa5b 100644 --- a/src/i18n/index.js +++ b/src/i18n/index.js @@ -18,8 +18,9 @@ import { } from './locale.js'; import es from './locales/es.json' with { type: 'json' }; import ru from './locales/ru.json' with { type: 'json' }; +import zhCN from './locales/zh-CN.json' with { type: 'json' }; -const CATALOGS = Object.freeze({ es, ru }); +const CATALOGS = Object.freeze({ es, ru, 'zh-CN': zhCN }); let currentLocale = DEFAULT_LOCALE; diff --git a/src/i18n/locale.js b/src/i18n/locale.js index 626303ad7..fab95e5cc 100644 --- a/src/i18n/locale.js +++ b/src/i18n/locale.js @@ -8,25 +8,32 @@ export const LOCALE_STORAGE_KEY = 'gev:locale:v1'; export const DEFAULT_LOCALE = 'en'; /** Shipped locales; order does not matter. Adding one: docs/TRANSLATORS.md. */ -export const LOCALES = Object.freeze(['en', 'es', 'ru']); +export const LOCALES = Object.freeze(['en', 'es', 'ru', 'zh-CN']); /** Each locale named in its own language, for the selector. */ export const LOCALE_NAMES = Object.freeze({ en: 'English', es: 'Español', ru: 'Русский', + 'zh-CN': '简体中文', }); /** Right-to-left locales. */ const RTL_LOCALES = Object.freeze([]); /** - * Map any language tag to a shipped locale: 'ru-RU' and 'RU' → 'ru'. + * Map any language tag to a shipped locale: 'ru-RU' and 'RU' → 'ru', + * 'zh', 'zh-CN' and 'zh-Hans-SG' → 'zh-CN'. * @returns {string|null} */ export function normalizeLocale(candidate) { if (typeof candidate !== 'string') return null; - const primary = candidate.trim().toLowerCase().split(/[-_]/)[0]; + const [primary, ...rest] = candidate.trim().toLowerCase().split(/[-_]/); + // Traditional Chinese readers are not given Simplified: no zh-TW yet. + if (primary === 'zh') + return rest.some((part) => ['hant', 'tw', 'hk', 'mo'].includes(part)) + ? null + : 'zh-CN'; return LOCALES.includes(primary) ? primary : null; } diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index f93f42070..b0d4dbef5 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -1096,7 +1096,7 @@ "FIELD REPORTS · {count}": "INFORMES DE CAMPO · {count}", "GEOLOCATIONS · {credit}": "GEOLOCALIZACIONES · {credit}", "↗ OPEN PUBLIC MAP": "↗ ABRIR MAPA PÚBLICO", - "Clouds are preserved from the source imagery.": "Las nubes provienen de las imágenes de origen.", + " Clouds are preserved from the source imagery.": " Las nubes provienen de las imágenes de origen.", "Show the Nepal context callout without a border or country fill": "Muestra la etiqueta de contexto de Nepal sin frontera ni relleno del país", "Introduce Nepal, draw its border, then reveal the Bhote Koshi incident": "Presenta Nepal, dibuja su frontera y luego revela el incidente del Bhote Koshi", "CITIES": "CIUDADES", @@ -2890,5 +2890,9 @@ "USB work abandoned after a stalled transfer": "Trabajo USB cancelado tras una transferencia bloqueada", "SDR decoder did not start; receiver stopped": "El decodificador SDR no arrancó; receptor detenido", "SDR decoder stopped responding; receiver stopped": "El decodificador SDR dejó de responder; receptor detenido", - "SDR decoder fell behind; receiver stopped": "El decodificador SDR se quedó atrás; receptor detenido" + "SDR decoder fell behind; receiver stopped": "El decodificador SDR se quedó atrás; receptor detenido", + "hemisphere|N": "N", + "hemisphere|S": "S", + "hemisphere|E": "E", + "hemisphere|W": "O" } diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 6a58ab901..cce27c3d4 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -1110,7 +1110,7 @@ "FIELD REPORTS · {count}": "СООБЩЕНИЯ С МЕСТА · {count}", "GEOLOCATIONS · {credit}": "ГЕОЛОКАЦИИ · {credit}", "↗ OPEN PUBLIC MAP": "↗ ОТКРЫТЬ ПУБЛИЧНУЮ КАРТУ", - "Clouds are preserved from the source imagery.": "Облака оставлены такими, как на исходных снимках.", + " Clouds are preserved from the source imagery.": " Облака оставлены такими, как на исходных снимках.", "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": "ГОРОДА", @@ -2957,5 +2957,9 @@ "USB work abandoned after a stalled transfer": "Работа с USB прервана: передача данных зависла", "SDR decoder did not start; receiver stopped": "Декодер SDR не запустился; приёмник остановлен", "SDR decoder stopped responding; receiver stopped": "Декодер SDR перестал отвечать; приёмник остановлен", - "SDR decoder fell behind; receiver stopped": "Декодер SDR не успевает; приёмник остановлен" + "SDR decoder fell behind; receiver stopped": "Декодер SDR не успевает; приёмник остановлен", + "hemisphere|N": "С", + "hemisphere|S": "Ю", + "hemisphere|E": "В", + "hemisphere|W": "З" } diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 3a5972058..0a56bc76a 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -7,12 +7,16 @@ }, "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...": "正在配置视图…", @@ -20,6 +24,9 @@ "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": "滑翔机", @@ -31,18 +38,86 @@ "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": "第二次滑坡", + "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-08-26", - "BHOTE KOSHI OUTBURST FLOOD": "BHOTE KOSHI 冰湖溃决洪水", + "BHOTE KOSHI OUTBURST FLOOD": "BHOTE KOSHI 溃决洪水", "Close event layer": "关闭事件图层", "OBSERVED IMAGERY": "实测影像", "SCHEMATIC CORRIDOR": "示意走廊", @@ -63,12 +138,13 @@ "↗ OPEN ORIGINAL": "↗ 打开原始来源", "⌖ LOWER GORGE": "⌖ 下游峡谷", "FIELD REPORTS · {count}": "现场报道 · {count}", + "GEOLOCATIONS · {credit}": "地理定位 · {credit}", "↗ OPEN PUBLIC MAP": "↗ 打开公共地图", - "Clouds are preserved from the source imagery.": " 云层按源影像原样保留。", + " 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": "历史参考 {before}%,灾后 {after}%", + "A historical reference {before} percent, B post-event {after} percent": "A 历史参考 {before}%,B 灾后 {after}%", "■ RELEASE CAMERA": "■ 释放相机", "Seek Nepal scene clock": "定位尼泊尔场景时钟", "NEPAL SCENE CLOCK": "尼泊尔场景时钟", @@ -86,7 +162,7 @@ "{count} SOURCES": { "other": "{count} 个来源" }, - "OUTSIDE IMAGERY SWIPE": "影像范围外滑动", + "OUTSIDE IMAGERY SWIPE": "不在影像对比范围内", "{scene} / AUTHORED SHOT": "{scene} / 排定镜头", "NEPAL FLOOD INCIDENT": "尼泊尔洪水事件", "Play {shot}": "播放 {shot}", @@ -94,38 +170,103 @@ "Play reconstruction": "播放重构", "Load previous scene shot": "载入上一个场景镜头", "Load next scene shot": "载入下一个场景镜头", + "SCHEMATIC CORRIDOR, NOT MODELED ARRIVAL TIME.": "示意走廊,并非模拟的到达时间。", "Rasuwa · 26 Aug 2026": "Rasuwa · 2026-08-26", "Event reconstruction": "事件重构", + "BHOTE KOSHI FLOOD INCIDENT": "BHOTE KOSHI 洪水事件", + "NEPAL · 26 AUG 2026": "尼泊尔 · 2026-08-26", + "USGS INITIAL REPORT": "USGS 初始报告", + "26 AUG 2026 · 08:37 NPT": "2026-08-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": "打开 USGS 关于 2026 年 8 月 26 日尼泊尔滑坡的初始报告", + "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 要素的类型都必须是 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.": "该文件包含无法读取的图像,或图像超过 1600 万像素上限。", + "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…": "正在获取地图站点…", + "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": "已加载地图站点", + "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": "附近有观测或地图标注的上下文", + "observed or mapped nearby context": "附近的观测或地图标注态势", "no observed or mapped objects in current feeds": "当前数据源中没有观测或地图标注的对象", "Draw": "绘制", "Directions": "路线导航", "Recent imagery": "近期影像", - "STATION": "电台", + "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": "可见", + "VISUAL": "目视可见", "Brightest naked-eye objects — CelesTrak visual group": "最亮的肉眼可见天体:CelesTrak visual 组", "COMMS": "通信", "Broadband constellation shell — shown only in DENSE mode": "宽带星座:仅在“密集”模式下显示", @@ -168,10 +309,10 @@ "NVG": "NVG", "FLIR": "FLIR", "ANIME": "动漫", - "NOIR": "黑色电影", + "NOIR": "黑白", "SNOW": "雪景", "ARCTIC": "北极", - "ANTARCTIC": "南极洲", + "ANTARCTIC": "南极", "NORTH AMERICA": "北美洲", "SOUTH AMERICA": "南美洲", "EUROPE": "欧洲", @@ -186,16 +327,51 @@ "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}", + "N": "北", + "S": "南", + "E": "东", + "W": "西", "POWER UP · {count} KEYS WAITING": { - "other": "启动 · {count} 个密钥待配置" + "other": "增强 · {count} 个密钥待配置" }, - "POWERED UP": "已启动", + "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": "每次最多保存 {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 登录状态,请重试。", @@ -206,6 +382,7 @@ "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)", @@ -217,6 +394,7 @@ "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", @@ -229,14 +407,15 @@ "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 登录…", + "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}中移除。正在重启——本页面将自动重新加载。", - "Places context + Street View fallback; optional separate key": "地点上下文 + 街景备用;可选的独立密钥", + "The photorealistic 3D planet, place search + Street View": "照片级 3D 地球、地点搜索 + 街景", + "Places context + Street View fallback; optional separate key": "地点信息 + 街景备用;可选的独立密钥", "Voice control — API key or ChatGPT OAuth": "语音控制——API 密钥或 ChatGPT OAuth", "Live ships, worldwide": "全球实时船舶", "Live active-fire detections": "实时活跃火点探测", @@ -244,51 +423,88 @@ "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": "放大以加载标绘摄像头", + "No ALPR data for this area — US and Canada only": "此区域没有 ALPR 数据——仅覆盖美国和加拿大", + "Zoom in to load mapped cameras": "放大以加载地图标注的摄像头", "Showing cached locations": "正在显示缓存位置", - "Coverage limited — zoom in": "覆盖受限 —— 请放大", + "Coverage limited — zoom in": "覆盖受限——请放大", "No ALPR data for this area": "此区域没有 ALPR 数据", - "None on screen — nearby cameras are outside the view": "屏幕上没有摄像头 —— 附近的摄像头在视野之外", + "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": "全局上下文已关闭", + "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": "全局上下文导航", + "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})", - "Open-source mapped/observed context. Missing broadcasts, unloaded map areas, or unmapped sites are not evidence of absence.": "开源的地图标注/观测上下文。缺失的广播、未加载的地图区域或未标注的站点不能作为不存在的证据。", + "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 以内", + "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": "电台", + "Station": "站点", "🚲 {bikes} avail · {docks} docks · {capacity} cap": "🚲 可用 {bikes} · 车桩 {docks} · 容量 {capacity}", "⚠️ Not installed": "⚠️ 未安装", "⚠️ Not renting": "⚠️ 暂停租车", @@ -297,25 +513,29 @@ "{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": "投影 关", + "PROJ MONITOR": "投影:监视器", + "PROJ OFF": "投影:关", "VIEWSHED": "可视域", "CAL {value}": "CAL {value}", "SRC {value}": "SRC {value}", - "{value} CONTEXT": "{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": "潜在热带气旋", "Hurricane": "飓风", "Tropical storm": "热带风暴", "Tropical depression": "热带低压", "Subtropical storm": "亚热带风暴", "Subtropical depression": "亚热带低压", - "Track and cone match this advisory": "路径与锥体与本份公告一致", + "Track and cone match this advisory": "路径和锥体与本期公告一致", "Track/cone awaiting advisory {number}": "路径/锥体等待第 {number} 号公告", "Track/cone unavailable": "路径/锥体不可用", "Cached advisory · stale source": "缓存公告 · 数据源已过期", @@ -330,26 +550,103 @@ "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 公报。锥体描述的是预报中心路径的不确定范围,不代表气旋尺寸或全部灾害区域。预报点标注的是数据源的前导小时数,不是按公告发布时刻推算的时间。几何图形贴地显示;显示高度并非天气高度。请以官方公告为准。", + "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": "骑行", - "{error} — try again in a moment": "{error} — 请稍后重试", - "Routing is rate limited — try again in a moment": "路线请求过于频繁 — 请稍后重试", + "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": "路线规划不可用", - "Head out on {road}": "沿{road}出发", + "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": "到达目的地", - "{mode} — reroute for {modeLower}": "{mode} — 按{modeLower}重新规划路线", + "Make a U-turn onto {road}": "掉头进入 {road}", + "Make a U-turn": "掉头", + "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 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 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(再次点击取消)", @@ -363,29 +660,33 @@ "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": "按两次 Escape 退出绘图", - "{name} is active — {leave}, then set {endpoint}": "{name} 正在使用 — {leave},然后设置 {endpoint}", + "press Escape twice to leave Draw": "按两次 Escape 退出“绘制”", + "{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", + "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": "路线服务无响应 — 路线规划不可用", + "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} 前", + "{value} ago": "{value}前", "sensor n/a": "传感器 n/a", "FIRE · {frp} MW": "火点 · {frp} MW", "NIGHT": "夜间", @@ -393,20 +694,62 @@ "FIRE": "火点", "FIRES": "火点", "max {value} MW": "最大 {value} MW", - "new {value}": "新增 {value}", + "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} 天", "Fire · FRP {frp} MW": "火点 · FRP {frp} MW", - "STALE · cached {age}": "过期 · 缓存于 {age}", + "STALE · cached {age}": "过期 · {age}前缓存", "refreshing...": "刷新中…", "loading...": "加载中…", "KEY REQUIRED": "需要密钥", "LIVE · updated {age}": "实时 · 更新于 {age}", - "loading mapped installation context": "正在加载地图标注设施上下文", - "Too many mapped sites in view to list them all": "视野内标绘地点过多,无法全部列出", - "Installation context unavailable": "设施背景信息不可用", + "Source snapshot time unavailable": "来源快照时间不可用", + "Live data unavailable": "实时数据不可用", + "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.": "平移或缩放后以及每分钟自动更新。公共数据源覆盖以视图中心为圆心、半径 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:扣除一架飞机,因此并非原始占比。每个网格需在 {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}", @@ -422,7 +765,7 @@ "UNSPECIFIED": "未指明", "+{count} additional payload records": "另有 {count} 条有效载荷记录", "PAYLOAD DATA UNAVAILABLE": "有效载荷数据不可用", - "{value} KM DOWNRANGE": "下射程 {value} KM", + "{value} KM DOWNRANGE": "射程 {value} km", "POSITION UNAVAILABLE": "位置不可用", "FLIGHT {count}": "第 {count} 次飞行", "REUSED": "复用", @@ -432,8 +775,8 @@ "DATE UNAVAILABLE": "日期不可用", "UNSPECIFIED OPERATOR": "运营方未指明", "Select {mission}": "选择 {mission}", - "{value} KM": "{value} KM", - "{value} KM/S": "{value} KM/S", + "{value} KM": "{value} km", + "{value} KM/S": "{value} km/s", "Selected Space Mission": "已选太空任务", "SELECTED SPACE MISSION": "已选太空任务", "Show all missions": "显示所有任务", @@ -463,7 +806,7 @@ "SHOW ALL / DESELECT": "显示全部 / 取消选择", "Pause replay": "暂停回放", "Cancel replay": "取消回放", - "EST. DOWNRANGE": "预估下射程", + "EST. DOWNRANGE": "预估射程", "PLANNED · ": "计划 · ", "SUPPLIED TRAJECTORY POINTS": "提供的轨迹点", "RECONSTRUCTED ESTIMATE": "重建估算", @@ -475,13 +818,24 @@ "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} FPM", + "POSITION {age}": "位置 {age}", + "POSITION {seconds} S AGO": "位置 {seconds} 秒前", + "{count} MSGS": { + "other": "{count} 条消息" + }, "Decoder feeds": "解码器数据源", "WebUSB + decoder feeds": "WebUSB + 解码器数据源", "opening receiver": "正在打开接收机", @@ -489,12 +843,25 @@ "WebUSB needs desktop Chrome or Edge": "WebUSB 需要桌面版 Chrome 或 Edge", "connect a receiver in Radio": "在电台面板连接接收机", "receiver is in FM mode": "接收机处于 FM 模式", - "listening": "聆听中", + "listening": "监听中", "USB error": "USB 错误", "Alt unknown": "高度未知", "Type unknown": "类型未知", "Reg unknown": "注册号未知", "Operator unknown": "运营方未知", + "${amount}K": "{amount} 千美元", + "${amount}M": "{amount} 百万美元", + "${amount}B": "{amount} 十亿美元", + "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} 英亩" }, @@ -505,7 +872,7 @@ "other": "{personnel} 名人员" }, "{county} County": "{county} 县", - "{cost} to date": "迄今损失 {cost}", + "{cost} to date": "迄今扑救费用 {cost}", "discovered {age} ago": "发现于 {age}前", "updated {age} ago": "更新于 {age}前", "part of {complex}": "属于 {complex} 复合火场", @@ -518,19 +885,32 @@ "Under 50% contained": "控制低于 50%", "50–99% contained": "控制 50–99%", "Fully contained": "已全部控制", - "Colour shows reported containment. Perimeters are simplified to about 100 m.": "颜色表示报告的控制比例。周界简化至约 100 米。", + "Colour shows reported containment. Perimeters are simplified to about 100 m.": "颜色表示报告的控制比例。周界简化至约 100 m。", + "Malformed perimeter snapshot": "周界快照格式错误", + "radio tag|All": "全部", "News": "新闻", - "Talk": "谈话", + "Talk": "访谈", "Weather / Emergency": "天气 / 应急", "Public Safety": "公共安全", "Aviation / Marine": "航空 / 海事", - "Traffic / Transit": "交通 / 公交", + "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": "蓝调", @@ -556,7 +936,32 @@ "Techno": "Techno", "Trance": "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": "Sentinel-2 与 Landsat,经 HLS,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}——点击重试", @@ -565,10 +970,66 @@ "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 地图:请在地图源(MAP SOURCE)中选择", + "zoom in to a street to open {name}": "放大到街道以打开 {name}", "KEY REJECTED": "密钥被拒绝", - "loading coverage...": "正在加载覆盖范围...", + "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 范围内没有街景影像", + "Coverage tile failed": "覆盖瓦片加载失败", + "Point the camera at the globe for street-level coverage": "将相机对准地球以查看街景覆盖范围", + "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": "将视野中心对准街道以查找该处影像", + "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,且本站点在允许的引荐来源(referrer)中", + "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 返回的响应无法读取", + "No street-level imagery providers configured": "未配置街景影像提供商", + "{names}: by upload date": "{names}:按上传日期", "Selected": "所选", + "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 道路不可用", @@ -579,25 +1040,71 @@ "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 路况", - "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}%", - "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(无匹配)", + "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 每日配额", + "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}%", + "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 密钥以获取实时数据", - "Route {route}": "航线{route}", + "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": "船舶", - "POS: LIVE": "位置:实时", - "POS: {time}Z": "位置:{time}Z", + "{speed}KT": "{speed} kn", + "{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 消息…", @@ -607,32 +1114,44 @@ "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 数据", + "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": "天气数据不可用", - "{hours}h {minutes}m ago": "{hours} 小时 {minutes} 分前", + "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": "全球红外 · 每小时", "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³", + "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": "减弱动效 · 历史回放不可用", @@ -647,38 +1166,50 @@ "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": "每小时全球拼图;通常延迟 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} 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.": "NOAA/NWS 15 分钟闪电密度,来源为 Vaisala NLDN/GLD360。覆盖自 110°E 经太平洋/美洲至 0°,25°S–80°N。不是实时闪击计数、不是全球覆盖,也不构成安全警示。", - "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 公里分辨率;显示限制在第 6 级。各帧使用数据源公布的精确观测时间。", + "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 米气温", + "Air temperature · 2 m": "气温 · 2 m", "Global · 1° grid": "全球 · 1° 网格", "{model} forecast · {time}": "{model} 预报 · {time}", "Loading forecast": "正在加载预报", @@ -703,32 +1234,69 @@ "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}", + "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 米高度的地表风。约 1° 全球网格。流线沿 10 米风场绘制,为便于观看抬升至 12 公里;显示高度并非天气高度。视图光照仅为可读性服务。动画展示的是单一固定预报内的流场,不推进时间。颜色字段贴附于球体底图或当前的照片级 3D Tiles。", + "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;显示高度并非天气高度。视图光照仅为可读性服务。动画展示的是单一固定预报内的流场,不推进时间。颜色字段贴附于球体底图或当前的照片级 3D Tiles。", "MODEL": "模型", "FIELD": "字段", "UNITS": "单位", "MOTION": "运动", "Forecast": "预报", + "NNE": "北东北", + "NE": "东北", + "ENE": "东东北", + "ESE": "东东南", + "SE": "东南", + "SSE": "南东南", + "SSW": "南西南", + "SW": "西南", + "WSW": "西西南", + "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° 网格插值的模式预报。反映大范围天气形势,不是街道级实测。", - " from {direction}": "从{direction}出发", + " · calm": " · 静风", + " from {direction}": ",{direction}风", "WIND AT {coordinates}": "{coordinates} 的风", - "{model} · valid {time}": "{model} · 生效 {time}", + "{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 瓦片不可用 — 请检查密钥的 API 限制、配额或网络", + "Google 3D tiles unavailable — check the key's API restrictions, quota, or network": "Google 3D 瓦片不可用——请检查密钥的 API 限制、配额或网络", + "Needs {envVars} — add it in Provider Settings — or a Cesium ion token for the ion-hosted route": "需要 {envVars}(请在服务商设置中添加),或使用 Cesium ion 令牌走 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.": "已保存的项目无法读取;存储已保留。请导入有效文件以恢复保存。", @@ -782,11 +1350,41 @@ "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.": "在你应用此文件之前,不会载入或更改任何内容。", @@ -815,35 +1413,115 @@ "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": "目标:最近的飞机 · 船舶 · 站点", + "CONTACTS — nearest planes · vessels · sites": "目标:最近的飞机 · 船舶 · 设施", "SPACE MISSIONS — launches & orbital assets": "太空任务:发射与在轨资产", "LIVE SIGNALS": "实时信号", "CCTV layer unavailable": "CCTV 图层不可用", "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": "范围", - "{value}m": "{value}分钟", + "HGT": "高度", + "ΔN": "ΔN", + "ΔE": "ΔE", + "{value}m": "{value} m", "CCTV calibration reset": "CCTV 校准已重置", "ADJUST ON": "调整开", "ADJUST": "调整", + "SNAPSHOT": "快照", "LIVE": "实时", + "UPSTREAM": "上游", + "SYNTHETIC": "合成", + "CONFIGURED": "已配置", + "SEED": "种子", "UNKNOWN": "未知", "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": "原始默认", + "RAW PRIOR": "初始估计", "CCTV ON": "CCTV 开", "CCTV OFF": "CCTV 关", "VIEWSHED ON": "可视域开", @@ -857,14 +1535,14 @@ "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}", + "{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 以激活" }, - "Enable CCTV to load camera intersections": "启用 CCTV 以加载摄像头路口", + "Enable CCTV to load camera intersections": "启用 CCTV 以加载路口摄像头", "Enable CCTV to start camera-linked intelligence summaries.": "启用 CCTV 以开始生成与摄像头关联的情报摘要。", "No summary available.": "暂无摘要。", "loading frames": "正在加载画面", @@ -884,7 +1562,7 @@ "MIXED CONDITIONS": "混合天气", "CYCLE ON": "轮换开", "CYCLE OFF": "轮换关", - "Stop automatic page cycling. Previous, Next, and the SIG/NEWS/LOCAL tabs stay available.": "停止自动翻页。上一页、下一页以及 SIG/NEWS/LOCAL 标签仍可使用。", + "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": "地区新闻不可用", @@ -892,12 +1570,15 @@ "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": "地区新闻", - "CONTEXT STANDBY": "上下文待机", - "ENABLE GLOBAL CONTEXT FOR PROXIMITY PINGS": "启用全局上下文以获取邻近 PING", + "{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": "目标已脱离其数据源 · 读数保持最后已知值", @@ -912,6 +1593,9 @@ "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": "地面数据备用", @@ -930,7 +1614,7 @@ "Night vision": "夜视", "Thermal": "热成像", "Anime": "动漫", - "Noir": "黑色电影", + "Noir": "黑白", "Snow": "雪景", "Current cockpit vision style: {style}. Activate for next style.": "当前座舱视觉样式:{style}。激活可切换到下一个样式。", "Current style: {style} — click for next": "当前样式:{style}——点击切换下一个", @@ -942,9 +1626,10 @@ "Expand cockpit briefing panel": "展开座舱简报面板", "Collapse briefing panel": "收起简报面板", "Expand briefing panel": "展开简报面板", - "OBSERVED / MAPPED PINGS": "观测 / 地图标注 PING", + "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": "时间未知", @@ -966,7 +1651,7 @@ "Reclassify as TR-3B": "重新归类为 TR-3B", "TRACK ACQUIRED": "已锁定航迹", "{label} · COURSE {heading}°": "{label} · 航向 {heading}°", - "Context could not restore every layer; try again": "上下文未能恢复所有图层,请重试", + "Context could not restore every layer; try again": "态势未能恢复所有图层,请重试", "No selected data layers": "没有已选数据图层", "{count} data layers could not be cleared": { "other": "{count} 个数据图层无法清除" @@ -980,18 +1665,40 @@ "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": "该图层在当前上下文模式下不可用", + "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": "上下文", + "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}", + "Fly to {name}": "飞往 {name}", + "Remove {name}": "移除 {name}", + "At most {count} files at once — remove one first.": "一次最多 {count} 个文件——请先移除一个。", + "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 小时)", @@ -1003,32 +1710,39 @@ "Bikeshare": "共享单车", "Live AIS Vessels": "实时 AIS 船舶", "Mapped Installations": "地图标注设施", - "Global Context": "全局上下文", + "Global Context": "全局态势", "Datacenters": "数据中心", "Dams": "水坝", "Submarine Cables": "海底电缆", "FIRMS Active Fires": "FIRMS 活跃火点", "Local ADS-B": "本地 ADS-B", - "Transit": "交通", + "Transit": "公共交通", "Recent Imagery": "近期影像", "Fire Perimeters": "火场周界", + "GNSS Integrity": "GNSS 完好性", "Rain radar": "降雨雷达", "Lightning density": "闪电密度", "Cyclone advisories": "气旋公告", "ALPR Cameras": "ALPR 摄像头", "Live Vessels": "实时船舶", - "Bike Share": "公共自行车", - "Cameras": "相机", + "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": "移动", + "Movement": "动态目标", "Infrastructure": "基础设施", "Events": "事件", "Weather": "天气", @@ -1038,7 +1752,7 @@ "UNCERTAIN · {source} · lifecycle state requires reconciliation": "不确定 · {source} · 生命周期状态需要核对", "{state} · {source} · {detail} · retry {seconds}s": "{state} · {source} · {detail} · {seconds} 秒后重试", "{state} · {source} · {detail}": "{state} · {source} · {detail}", - "never": "从未", + "never": "未更新", "{source} · {loadingLabel}": "{source} · {loadingLabel}", "{accepted} of {raw} records accepted": "已接受 {accepted} / {raw} 条记录", "incomplete snapshot": "快照不完整", @@ -1047,20 +1761,33 @@ "{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": "正在流式接收", + "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} 个模块" + "other": "DSP {count} 个数据块" }, "DSP waiting": "DSP 等待中", + "{level} dBFS audio": "音频 {level} dBFS", "audio signal --": "音频信号 --", "audio {state}": "音频 {state}", "{count} HEARD": { @@ -1072,7 +1799,12 @@ "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": "已搜索的位置", @@ -1093,7 +1825,9 @@ "Collapse Radio": "收起电台", "Expand Radio section": "展开电台区域", "Collapse Radio section": "收起电台区域", + "Double-click the header to snap the panel back": "双击标题栏可将面板复位", "Panel layout updated — positions reset to new defaults": "面板布局已更新——位置已重置为新的默认值", + "Drag to resize · double-click the header to snap back": "拖动以调整大小 · 双击标题栏复位", "Austin": "奥斯汀", "San Francisco": "旧金山", "New York": "纽约", @@ -1103,6 +1837,7 @@ "Dubai": "迪拜", "Washington DC": "华盛顿特区", "Tallinn": "塔林", + "Los Angeles": "洛杉矶", "Collapse Cockpit display options": "收起座舱显示选项", "Expand Cockpit display options": "展开座舱显示选项", "Collapse Cockpit Radio controls": "收起座舱电台控件", @@ -1116,8 +1851,8 @@ "STATION UNAVAILABLE": "电台不可用", "Station unavailable after directory refresh": "目录刷新后该电台不可用", "Radio could not {action} cleanly": "电台未能正常{action}", - "Go to expanded Radio section": "前往展开的电台分区", - "Expand Radio section in Context": "在态势面板中展开电台分区", + "Go to expanded Radio section": "前往展开的电台区域", + "Expand Radio section in Context": "在态势面板中展开电台区域", "Close compact Radio controls": "关闭精简电台控件", "Open compact Radio controls": "打开精简电台控件", "SYNC": "同步", @@ -1128,7 +1863,7 @@ "Disable Radio": "停用电台", "Enable Radio": "启用电台", "DIRECTORY BAND": "目录频段", - "{category} BAND": "{category}频段", + "{category} BAND": "{category} 频段", "NO STATION SELECTED": "未选择电台", "Directory metadata only": "仅目录元数据", "Loading station directory…": "正在加载电台目录…", @@ -1174,7 +1909,12 @@ "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": "此框选最新的低云日 · 云量为场景云量,而非框内云量", @@ -1187,6 +1927,8 @@ "newest overview": "最新概览", "No imagery": "无影像", "Loading": "加载中", + "coverage|full": "完整", + "coverage|partial": "部分", "unknown": "未知", "no imagery": "无影像", "{percent}% cloud": "{percent}% 云量", @@ -1215,7 +1957,7 @@ "Trade the two sides of the divider": "互换分割线两侧", "SELECT BOX": "框选区域", "Drag a box on the map (Esc cancels)": "在地图上拖出框选(Esc 取消)", - "USE VIEW": "使用当前视野", + "USE VIEW": "使用视野", "Use the current view as the box": "以当前视野作为框选", "Forget the box and its images": "清除框选及其影像", "START HERE": "从此开始", @@ -1225,19 +1967,24 @@ "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}", @@ -1264,14 +2011,29 @@ "Stopped (Esc)": "已停止(Esc)", "SPARSE": "稀疏", "BALANCED": "均衡", - "No shots yet. Use CAPTURE SHOT to save current look.": "暂无镜头。使用“采集镜头”保存当前画面。", + "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}——数据源不可用", @@ -1284,9 +2046,9 @@ "LOAD CANCELLED": "加载已取消", "LOAD FAILED": "加载失败", "LIVE DATA OFF": "实时数据已关闭", - "MAPPED SITES LOADED": "已加载地图站点", - "RETRYING MAPPED SITES": "正在重试地图站点", - "FETCHING MAPPED SITES": "正在获取地图站点", + "MAPPED SITES LOADED": "已加载地图标注设施", + "RETRYING MAPPED SITES": "正在重试地图标注设施", + "FETCHING MAPPED SITES": "正在获取地图标注设施", "RETRYING ALPR CAMERAS": "正在重试 ALPR 摄像头", "FETCHING ALPR CAMERAS": "正在获取 ALPR 摄像头", "OVERPASS TEMPORARILY UNAVAILABLE": "Overpass 暂时不可用", @@ -1301,11 +2063,48 @@ "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": "{count} 个 CelesTrak 分组不可用" + }, + "{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 could not start": "街景未能启动", "Street Level toggle failed": "街景切换失败", "Street-level image": "街景图像", @@ -1332,15 +2131,38 @@ "{name} imagery off": "{name} 影像已关闭", "Image by {creator}": "图像作者:{creator}", "Camera follows view: move the globe camera wherever the street-level view looks": "相机跟随视野:街景看向哪里,地球相机就移到哪里", - "Camera follow needs the Google 3D map: choose Google 3D under MAP SOURCE": "相机跟随需要 Google 3D 地图:请在地图源(MAP SOURCE)中选择 Google 3D", + "Camera follow needs the Google 3D map: choose Google 3D under MAP SOURCE": "相机跟随需要 Google 3D 地图:请在“地图源”中选择 Google 3D", + " or ": " 或 ", + "OPEN {labels}": "打开 {labels}", + "Open {names} at the centre of the view": "在视野中心打开 {names}", + "Zoom in to a street to open {names} here": "放大到街道以在此处打开 {names}", "Switch a provider on to draw its coverage.": "开启一个提供商以绘制其覆盖范围。", "Loading this sequence…": "正在加载此序列…", "{n} images in this sequence · Esc clears": { "other": "此序列共 {n} 张图像 · Esc 清除" }, + "{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": "kn", + "kts": "kn", + "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}", @@ -1350,9 +2172,16 @@ "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": "错误", "AI AGENT": "AI 代理", "Voice model tier — applies next session": "语音模型档位——下次会话生效", + "STD": "标准", "Estimated session cost": "预估会话费用", "Voice control — activate to toggle voice; hold Space to speak": "语音控制——激活可切换语音;按住空格键说话", "ON/OFF": "开/关", @@ -1363,13 +2192,14 @@ "VOICE": "语音", "Dismiss voice card": "关闭语音卡片", "YOU": "用户", - "THIS": "此目标", + "THIS": "目标", "Plan": "计划", "Referenced items": "引用条目", "NOTES & SOURCES": "笔记与来源", "VOICE SYSTEM ERROR": "语音系统错误", "DISMISS": "关闭", "Check microphone permission and network access, then try again.": "请检查麦克风权限和网络连接,然后重试。", + "Stopped by voice": "已通过语音停止", "WebRTC microphone support unavailable": "不支持 WebRTC 麦克风", "Requesting microphone": "正在请求麦克风", "Release Space to send": "松开空格键发送", @@ -1379,32 +2209,99 @@ "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} 个输入 token、{output} 个输出 token、{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.": "估算不完整 — 会话结束时仍有响应在处理中,因此其用量从未上报。", + "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": "电台未能启动", "Running command": "正在执行指令", + "{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": "{n} 个 Flock 摄像头" + }, + "{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}": "{items} {scope}", + "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.": "该计数为下限,因为至少一个图层已达到其保留上限。", "{label} off": "{label} 已关闭", + "{label} loading": "{label} 加载中", + "{label} error": "{label} 出错", "counted the first {returned} loaded records": "已统计最先加载的 {returned} 条记录", "counted {returned} of {total} loaded records": "已统计已加载 {total} 条记录中的 {returned} 条", - "Fly below {km} km to a covered region": "请飞行至 {km} 公里以下的覆盖区域", + "{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} 个覆盖区域" + "other": "此处暂无数据源 · 有 {n} 个覆盖区域" }, "degraded": "降级", "stale": "过期", @@ -1416,21 +2313,72 @@ "Activate to toggle voice": "点击切换语音", "Voice active": "语音已启用", "Voice could not be started.": "无法启动语音。", + "the active voice session": "当前语音会话", "{value} feet": "{value} 英尺", + "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-live-vessels 图层未开启", + "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}内", + "Camera range {range}": "相机距离 {range}", + "Within {radius} of the view": "距当前视野 {radius} 内", "Labels on": "标签已开启", "Framing uses the nearest 80 loaded contacts": "取景仅使用已加载的最近 80 个目标", - "{distance} from {location}": "{distance} 自 {location}", - "Altitude {altitude}": "高度{altitude}", + "{distance} from {location}": "距 {location} {distance}", + "Altitude {altitude}": "高度 {altitude}", "{origin} to {destination}": "{origin} 至 {destination}", - "from {origin}": "从{origin}出发", - "to {destination}": "前往{destination}", - "Registration {registration}": "注册号{registration}", + "from {origin}": "从 {origin} 出发", + "to {destination}": "前往 {destination}", + "Registration {registration}": "注册号 {registration}", "View state": "视野状态", "Marked {count} places": { "other": "已标记 {count} 个地点" @@ -1438,10 +2386,42 @@ "Nothing marked": "没有标记", "Not found: {places}": "未找到:{places}", "Tracing outlines": "正在描画边界", - "Frame {target}": "取景{target}", + "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": "在地图上标注", - "Fly to {place}": "飞往{place}", - "Nearest aircraft to {place}": "距离{place}最近的飞机", + "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": "检查视野状态", @@ -1465,13 +2445,13 @@ "DONE": "完成", "HERE": "此处", "Aircraft cockpit view": "飞机座舱视图", - "LEVEL": "等级", + "LEVEL": "水平", "Estimated destination direction": "预估目的地方向", "DEST ---°": "目的地 ---°", "OPTICAL PLANE · 01": "光学平面 · 01", "VISOR LOCK · ACTIVE": "面罩锁定 · 激活", - "GROUND SPEED · KTS": "地速 · KTS", - "ALTITUDE · FT": "高度 · FT", + "GROUND SPEED · KTS": "地速 · kn", + "ALTITUDE · FT": "高度 · ft", "FIRST PERSON": "第一人称", "LIVE TRACK · COURSE ALIGNED": "实时航迹 · 航向对准", "Cockpit vision style": "座舱视觉样式", @@ -1484,15 +2464,21 @@ "Next vision style": "下一个视觉样式", "Current aircraft heading": "飞机当前航向", "GROUND SPEED": "地速", + "KTS": "kn", "ALTITUDE": "高度", + "FT": "ft", "Contact cockpit summary": "目标座舱摘要", "CONTACT": "目标", - "CONTACTS · 250 KM": "目标 · 250 KM", + "CONTACTS · 250 KM": "目标 · 250 km", "Contact navigation": "目标导航", "CONTEXT ONLY": "仅供参考", "Nearby cohort counts": "附近各类目标数量", + "FLT": "航班", + "MIL": "军用", + "SITE": "设施", "NEAREST OBSERVED / MAPPED": "最近的观测 / 地图标注目标", "AVAILABLE INPUTS ONLY · NOT AN ALL-CLEAR": "仅限可用输入 · 不代表安全无虞", + "WX": "天气", "Cockpit briefing carousel": "座舱简报轮播", "Estimated flight plan": "预估飞行计划", "ESTIMATED FLIGHT PLAN": "预估飞行计划", @@ -1508,8 +2494,9 @@ "WIND": "风", "SKY": "天空", "PRECIP": "降水", + "MM": "mm", "Weather data by Open-Meteo.com": "天气数据由 Open-Meteo.com 提供", - "Cockpit briefing pages": "驾驶舱简报页面", + "Cockpit briefing pages": "座舱简报页面", "SIG": "信号", "Show Live Signals": "显示实时信号", "NEWS": "新闻", @@ -1542,7 +2529,7 @@ "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.": "应用高对比度的黑白黑色电影调色。", + "Apply high-contrast monochrome film-noir grading.": "应用高对比度的黑白电影风格调色。", "Add a cold, snowy whiteout treatment to the scene.": "为场景添加寒冷的雪天白茫茫效果。", "MAP SOURCE": "地图源", "Map source": "地图源", @@ -1557,24 +2544,24 @@ "WEATHER": "天气", "Active weather products": "活跃天气产品", "RECENT IMAGERY": "近期影像", - "Imagery days in the box": "影像盒内的天数", - "CONTEXT": "上下文", + "Imagery days in the box": "框选内的影像日期", + "CONTEXT": "态势", "Compact Radio controls": "精简电台控件", "Open detailed Radio controls": "打开详细电台控件", "Previous station": "上一个电台", "Next station": "下一个电台", "Compact Radio volume": "精简电台音量", "Expand panel": "展开面板", - "Context mode": "上下文模式", + "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": "目标上下文操作", + "SELECT CONTEXT": "选择态势", + "Contact Context actions": "目标态势操作", "COCKPIT": "座舱", - "SEARCH NEARBY SITES": "搜索附近站点", + "SEARCH NEARBY SITES": "搜索附近设施", "Reclassify tracked contact as TR-3B": "将跟踪目标重新归类为 TR-3B", - "CONTACTS CONTEXT OFF": "目标上下文已关闭", + "CONTACTS CONTEXT OFF": "目标态势已关闭", "SELECT CONTACTS TO LOAD OBSERVED / MAPPED PROXIMITY": "选择目标以加载观测 / 地图标注的邻近信息", "Available Space Missions": "可用太空任务", "AVAILABLE MISSIONS": "可用任务", @@ -1610,7 +2597,7 @@ "GAIN": "增益", "RTL-SDR tuner gain for the current mode": "当前模式下的 RTL-SDR 调谐器增益", "AUTO": "自动", - "MSG/S": "消息/秒", + "MSG/S": "msg/s", "HEARD": "已收到", "POSITIONED": "已定位", "FM MHZ": "FM MHz", @@ -1628,7 +2615,7 @@ "Minimal": "精简", "Cyber": "赛博", "Sonar": "声呐", - "Cyber sonar contact scanner": "赛博声呐接触扫描器", + "Cyber sonar contact scanner": "赛博声呐目标扫描器", "Rings": "圈数", "Sonar ring count": "声呐圈数", "Range": "范围", @@ -1636,8 +2623,8 @@ "Power": "强度", "Sonar intensity": "声呐强度", "Opacity": "不透明度", - "Cyber contact and label opacity": "赛博接触与标注不透明度", - "Minimum opacity for map dots, icons, models, brackets, and labels between sonar passes": "声呐扫描期间地图点、图标、模型、括标与标注的最低不透明度", + "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)", @@ -1663,23 +2650,26 @@ "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 取消", + "Draw on the world — click vertices, double-click or Enter to finish, Esc to cancel": "在地球上绘制——单击顶点,双击或按 Enter 完成,按 Esc 取消", "Shape": "形状", "Shape to draw": "要绘制的形状", "Area": "面", "Line": "线", - "Pin": "标记", + "Pin": "图钉", "Label (optional)": "标签(可选)", "Label for the drawn shape": "绘制形状的标签", "Colour of the drawn shape": "绘制形状的颜色", - "Primary": "主色", - "Amber": "琥珀", + "Primary": "主题色", + "Amber": "琥珀色", "Cyan": "青色", "Green": "绿色", "Red": "红色", - "Clear": "晴", + "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": "隐藏界面控件", "Clean UI": "纯净界面", @@ -1692,6 +2682,7 @@ "Return UI controls": "恢复界面控件", "Initializing photorealistic world...": "正在初始化逼真世界…", "DATA LAYERS": "数据图层", + "ACQUIRING FRAME": "正在获取画面", "NEAREST": "最近", "CCTV camera": "CCTV 摄像头", "CALIBRATION": "校准", @@ -1708,6 +2699,7 @@ "RESET CAL": "重置校准", "SCENE SUMMARY": "场景摘要", "STREET LEVEL": "街景", + "Turn Street Level on or off": "开启或关闭街景", "Expand the street-level view (Esc shrinks it)": "放大街景视图(Esc 缩小)", "Image fit": "图像适配", "FIT": "适应", @@ -1727,7 +2719,7 @@ "Panorama filter": "全景筛选", "ALL": "全部", "FLAT": "平面", - "SINCE": "时间范围", + "SINCE": "时间", "Coverage colours": "覆盖图例", "SCENES": "场景", "Scene recipe": "场景方案", @@ -1738,10 +2730,10 @@ "EXPORT PRESETS": "导出预设", "IMPORT": "导入", "RUN LOG": "运行日志", - "POWER UP": "启动", + "POWER UP": "增强", "GROUND STATION · PROVIDER SETTINGS": "地面站 · 服务商设置", "Close key setup": "关闭密钥设置", - "Power up the globe": "启动地球", + "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 关闭", @@ -1764,5 +2756,9 @@ "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.": "提示:停靠栏中的 GEV MIC 按钮可让你用语音操控地图。" + "Tip: the GEV MIC button in the dock lets you talk to the map.": "提示:停靠栏中的 GEV 麦克风按钮可让你用语音操控地图。", + "hemisphere|N": "N", + "hemisphere|S": "S", + "hemisphere|E": "E", + "hemisphere|W": "W" } diff --git a/src/i18n/same-as-english.json b/src/i18n/same-as-english.json index c159beaab..821b154b9 100644 --- a/src/i18n/same-as-english.json +++ b/src/i18n/same-as-english.json @@ -98,6 +98,9 @@ "fire confidence|nominal", "ft", "hPa", + "hemisphere|E", + "hemisphere|N", + "hemisphere|S", "installation class|BASE", "km", "km/h", @@ -133,5 +136,60 @@ "REC", "VS", "WHOT/BHOT" + ], + "zh-CN": [ + "1090 MHz", + "978 MHz UAT", + "AIS: --", + "AIS: {name}", + "BRG {value}°", + "BRG —", + "CAL {value}", + "CAL · {badge}", + "CCTV", + "CRS {value}°", + "CRT", + "FLIR", + "FOV", + "FOV {value}°", + "FT", + "GEO", + "HDG", + "HDG {value}°", + "House", + "Ironbow", + "KTS", + "Landsat 8/9", + "MM", + "NVG", + "REC", + "SRC {value}", + "Sentinel-2", + "Techno", + "Trance", + "USB {rate} msg/s", + "WHOT/BHOT", + "dB", + "ft", + "hPa", + "hemisphere|E", + "hemisphere|N", + "hemisphere|S", + "hemisphere|W", + "km", + "km/h", + "kt", + "kts", + "mph", + "{city} CCTV", + "{rate} msg/s", + "{source} HTTP {status}", + "{speed} kn", + "{type} SPD: {speed} HDG: {heading}", + "{value} KM", + "{value} KM/S", + "ΔE", + "ΔN", + "▲ {frp} MW" ] } diff --git a/src/ui/localePicker.test.mjs b/src/ui/localePicker.test.mjs index 35b473e42..7f6d11f9b 100644 --- a/src/ui/localePicker.test.mjs +++ b/src/ui/localePicker.test.mjs @@ -40,7 +40,7 @@ test('the current language comes first, the rest by their own names', () => { nativeNames: LOCALE_NAMES, }), ), - ['ru', 'en', 'es'], + ['ru', 'en', 'es', 'zh-CN'], ); assert.deepEqual( codes( @@ -50,7 +50,7 @@ test('the current language comes first, the rest by their own names', () => { nativeNames: LOCALE_NAMES, }), ), - ['es', 'en', 'ru'], + ['es', 'en', 'ru', 'zh-CN'], ); assert.deepEqual( codes( @@ -60,7 +60,7 @@ test('the current language comes first, the rest by their own names', () => { nativeNames: LOCALE_NAMES, }), ), - ['en', 'es', 'ru'], + ['en', 'es', 'ru', 'zh-CN'], ); const rows = buildLocaleOptions({ locales: MANY, @@ -263,7 +263,7 @@ test('keyboard model: open on the current language, move, choose, close', () => assert.deepEqual(model.keydown({ key: 'ArrowDown' }), { handled: true }); assert.equal(model.activeOption.code, 'en'); model.keydown({ key: 'End' }); - assert.equal(model.activeOption.code, 'es'); + assert.equal(model.activeOption.code, 'zh-CN'); model.keydown({ key: 'Home' }); assert.equal(model.activeOption.code, 'ru'); model.keydown({ key: 'ArrowUp' }); diff --git a/src/ui/styles/cjk.css b/src/ui/styles/cjk.css new file mode 100644 index 000000000..c44431520 --- /dev/null +++ b/src/ui/styles/cjk.css @@ -0,0 +1,29 @@ +/* Chinese text. The UI sets Latin capitals with wide tracking; Chinese set + that way falls apart into single glyphs, so its tracking stays tight. The + intel HUD readouts (codes, coordinates) and text kept in English on purpose + (translate="no", and whatever holds it) keep the Latin look. */ +html:lang(zh) + body + :not( + #intel-hud, + #intel-hud *, + [translate='no'], + [translate='no'] *, + :has([translate='no']) + ) { + letter-spacing: 0.02em !important; +} + +/* Below about 9px a Chinese glyph blurs: lift the dock's voice labels and the + panel layer buttons to that floor. */ +html:lang(zh) #command-dock .gev-mic-label, +html:lang(zh) #command-dock #gev-voice-detail, +html:lang(zh) .gev-voice-tier-btn, +html:lang(zh) .panel-layer-toggle { + font-size: 9px; +} + +/* A two-glyph button label must not break between its glyphs. */ +html:lang(zh) .pp-mode-btn { + white-space: nowrap; +} diff --git a/style.css b/style.css index 44b7e07bc..8c4599e89 100644 --- a/style.css +++ b/style.css @@ -27,4 +27,5 @@ @import './src/ui/styles/weather.css'; @import './src/ui/styles/panel-surfaces.css'; +@import './src/ui/styles/cjk.css'; @import './src/ui/styles/cyber.css'; From f3fa1fa0227b7519c29834d42d3e79ae202e77d2 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Wed, 21 Oct 2026 12:54:55 +0400 Subject: [PATCH 16/25] i18n(zh-CN): translate the strings from the latest main Gemini Live voice and the street-level coverage hints; drop the entries the code no longer uses. Co-Authored-By: Claude Opus 5.5 --- src/i18n/locales/zh-CN.json | 73 ++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 0a56bc76a..a83d35cc0 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -980,7 +980,6 @@ "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 范围内没有街景影像", "Coverage tile failed": "覆盖瓦片加载失败", - "Point the camera at the globe for street-level coverage": "将相机对准地球以查看街景覆盖范围", "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} 街景照片", @@ -1026,7 +1025,6 @@ "Panoramax sent an unreadable answer": "Panoramax 返回的响应无法读取", "No street-level imagery providers configured": "未配置街景影像提供商", "{names}: by upload date": "{names}:按上传日期", - "Selected": "所选", "Open the Street Level panel to view imagery": "打开街景面板以查看影像", "Image could not be opened": "图像无法打开", "TeleGeography load failed": "TeleGeography 加载失败", @@ -1825,9 +1823,7 @@ "Collapse Radio": "收起电台", "Expand Radio section": "展开电台区域", "Collapse Radio section": "收起电台区域", - "Double-click the header to snap the panel back": "双击标题栏可将面板复位", "Panel layout updated — positions reset to new defaults": "面板布局已更新——位置已重置为新的默认值", - "Drag to resize · double-click the header to snap back": "拖动以调整大小 · 双击标题栏复位", "Austin": "奥斯汀", "San Francisco": "旧金山", "New York": "纽约", @@ -2107,7 +2103,6 @@ "{feed} {count} ({seconds}s old)": "{feed} {count}({seconds} 秒前)", "Street Level could not start": "街景未能启动", "Street Level toggle failed": "街景切换失败", - "Street-level image": "街景图像", "SHRINK": "缩小", "EXPAND": "展开", "Shrink": "缩小", @@ -2130,13 +2125,10 @@ "{name} imagery on": "{name} 影像已开启", "{name} imagery off": "{name} 影像已关闭", "Image by {creator}": "图像作者:{creator}", - "Camera follows view: move the globe camera wherever the street-level view looks": "相机跟随视野:街景看向哪里,地球相机就移到哪里", - "Camera follow needs the Google 3D map: choose Google 3D under MAP SOURCE": "相机跟随需要 Google 3D 地图:请在“地图源”中选择 Google 3D", " or ": " 或 ", "OPEN {labels}": "打开 {labels}", "Open {names} at the centre of the view": "在视野中心打开 {names}", "Zoom in to a street to open {names} here": "放大到街道以在此处打开 {names}", - "Switch a provider on to draw its coverage.": "开启一个提供商以绘制其覆盖范围。", "Loading this sequence…": "正在加载此序列…", "{n} images in this sequence · Esc clears": { "other": "此序列共 {n} 张图像 · Esc 清除" @@ -2700,14 +2692,11 @@ "SCENE SUMMARY": "场景摘要", "STREET LEVEL": "街景", "Turn Street Level on or off": "开启或关闭街景", - "Expand the street-level view (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": "填充:图像铺满画框并裁剪照片", - "Camera follows view": "相机跟随视野", - "FOLLOW": "跟随", "Close the image and deselect it on the map": "关闭图像并取消其在地图上的选中", "Close the street-level image": "关闭街景图像", "Street-level viewer": "街景查看器", @@ -2720,7 +2709,6 @@ "ALL": "全部", "FLAT": "平面", "SINCE": "时间", - "Coverage colours": "覆盖图例", "SCENES": "场景", "Scene recipe": "场景方案", "NEW": "新建", @@ -2760,5 +2748,64 @@ "hemisphere|N": "N", "hemisphere|S": "S", "hemisphere|E": "E", - "hemisphere|W": "W" + "hemisphere|W": "W", + "Gemini Live voice control": "Gemini Live 语音控制", + "Zoom in to see street-level coverage": "放大地图以查看街景覆盖范围", + "Point the camera at the ground for street-level coverage": "将相机朝向地面以查看街景覆盖范围", + "Sequence images unavailable": "序列影像不可用", + "Switch Street Level on to draw its coverage.": "开启“街景”以绘制其覆盖范围。", + "Every provider is switched off: light a chip to draw its coverage.": "所有服务商均已关闭:点亮一个标签即可绘制其覆盖范围。", + "No sequences in view match the filter": "视野内没有符合筛选条件的序列", + "No coverage in view": "视野内没有覆盖", + "Voice provider": "语音服务商", + "Changing provider reconnects voice if it is on.": "如语音已开启,切换服务商会重新连接语音。", + "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 令牌请求", + "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 会话设置与输入模式不匹配", + "Gemini could not submit this input. Start voice again.": "Gemini 无法提交此输入。请重新启动语音。", + "Connecting to Gemini": "正在连接 Gemini", + "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 请求的同时操作过多", + "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 连接即将到期。请重新启动语音以开始新对话。", + "Gemini voice sessions have a time limit. When a session expires, start voice again for a new conversation.": "Gemini 语音会话有时长限制。会话到期后,请重新启动语音以开始新对话。", + "Show the street-level view full screen (Esc shrinks it)": "全屏显示街景视图(按 Esc 缩小)" } From da1dd928efc5bb35cd3e4da9740bb93d6fe98088 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Wed, 21 Oct 2026 12:55:21 +0400 Subject: [PATCH 17/25] docs(changelog): Simplified Chinese (#1298) Co-Authored-By: Claude Opus 5.5 --- changelog.d/1298-simplified-chinese.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changelog.d/1298-simplified-chinese.md diff --git a/changelog.d/1298-simplified-chinese.md b/changelog.d/1298-simplified-chinese.md new file mode 100644 index 000000000..ffae0cd41 --- /dev/null +++ b/changelog.d/1298-simplified-chinese.md @@ -0,0 +1,6 @@ +- Add Simplified Chinese (zh-CN) as an interface language, selectable from + the globe picker. The catalog builds on the Chinese translations by + abiyeenzo (#914) and EDIT-Aeterna (#979); the rest was translated and the + whole catalog reviewed against the screens. Chinese text gets tight tracking + and readable minimum sizes, and year-first dates read 2021年10月16日 + (German, @gekh, #1298). From 6a40b30d12ce506e47a229d76f5d05f7534ff611 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Wed, 21 Oct 2026 13:02:34 +0400 Subject: [PATCH 18/25] docs(changelog): credit the zh-CN translators by GitHub handle Enzo's commit email is not linked to his GitHub account, so credit his #914 translation here as well. Co-authored-by: Enzo Abyie <93532759+abiyeenzo@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- changelog.d/1298-simplified-chinese.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/1298-simplified-chinese.md b/changelog.d/1298-simplified-chinese.md index ffae0cd41..e6850e964 100644 --- a/changelog.d/1298-simplified-chinese.md +++ b/changelog.d/1298-simplified-chinese.md @@ -1,6 +1,6 @@ - Add Simplified Chinese (zh-CN) as an interface language, selectable from the globe picker. The catalog builds on the Chinese translations by - abiyeenzo (#914) and EDIT-Aeterna (#979); the rest was translated and the + @abiyeenzo (#914) and @EDIT-Aeterna (#979); the rest was translated and the whole catalog reviewed against the screens. Chinese text gets tight tracking and readable minimum sizes, and year-first dates read 2021年10月16日 (German, @gekh, #1298). From d6c4d58a2da252c1ebeca2f682c1164e2d57255a Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Wed, 21 Oct 2026 13:35:52 +0400 Subject: [PATCH 19/25] i18n(zh-CN): apply the language review Correct weather, imagery, scene, voice, and status wording after the Chinese UI review. Align the translation glossary with the revised terminology. Co-Authored-By: Codex (GPT-6) --- docs/translations/zh-CN.md | 22 ++- src/i18n/locales/zh-CN.json | 332 ++++++++++++++++++------------------ 2 files changed, 187 insertions(+), 167 deletions(-) diff --git a/docs/translations/zh-CN.md b/docs/translations/zh-CN.md index 9b779e0b7..33f7c7251 100644 --- a/docs/translations/zh-CN.md +++ b/docs/translations/zh-CN.md @@ -46,10 +46,11 @@ for the current English and reviewed against both. | preset / visual preset | 预设 / 视觉预设 | | Display / HUD layout | 显示 / HUD 布局 | | Tactical / Operator / Minimal / Cyber | 战术 / 操作员 / 精简 / 赛博 | -| Scope (circular viewport mask) | 视域 | +| Scope (circular viewport mask) / keyhole | 视域 | | Clean UI | 纯净界面 | | cockpit | 座舱 | | heading / altitude / ground speed | 航向 / 高度 / 地速 | +| map heading / North up | 方位 / 正北朝上 | | Street Level / Street View (Google) | 街景 / 街景(Google) | | Recent Imagery | 近期影像 | | Global Context / Context (panel) | 全局态势 / 态势 | @@ -57,3 +58,22 @@ for the current English and reviewed against both. | transit / bikeshare | 公共交通 / 共享单车 | | Previous / Next | 上一个 / 下一个 | | compass points (N, NNE, …) | 北, 北东北, … | +| weather field / FIELD (selector) | 气象场 / 要素 | +| weather model / forecast lead hours | 模式 / 预报时效 | +| valid (forecast time) | 有效时间 | +| scene cloud / mosaic | 整景云量 / 镶嵌图 | +| pin (Recent Imagery) | 固定 | +| THIS (voice pointer) | 所指 | +| outline / trace an outline | 轮廓 / 绘制轮廓 | +| mark (map annotation) | 标注 | +| camera badge | 摄像头标记 | +| provider / embedded provider controls | 服务商 / 平台控件 | +| share (a view or link) | 分享 | +| AI agent | AI 智能体 | +| stage (rocket) / downrange | 子级 / 航程 | +| capture shot / camera move | 保存镜头 / 运镜 | +| story beat / evidence beat | 事件节点 / 证据节点 | +| camera pose | 位姿 | +| forecast uncertainty cone | 预报不确定范围 | +| seed (catalog data) | 预置 | +| frame (bring into view) | 将…纳入视野 | diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index a83d35cc0..8b5240e12 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -18,7 +18,7 @@ "{owner} is using the pointer — close it first.": "{owner} 正在使用指针——请先关闭它。", "{label} · Detailed outline unavailable": "{label} · 无法获取详细轮廓", "Flying to Austin, TX...": "正在飞往德克萨斯州奥斯汀…", - "Restoring shared view...": "正在恢复共享视图…", + "Restoring shared view...": "正在恢复分享视图…", "Configuring viewer...": "正在配置视图…", "Loading Google 3D Tiles...": "正在加载 Google 3D Tiles…", "Loading the keyless globe...": "正在加载免密钥地球…", @@ -31,7 +31,7 @@ "Light aircraft": "轻型飞机", "Glider": "滑翔机", "Turboprop": "涡桨飞机", - "Airliner": "喷气客机", + "Airliner": "民航客机", "Widebody": "宽体机", "Four-engine jet": "四发喷气机", "Helicopter": "直升机", @@ -48,10 +48,10 @@ "Bhote Koshi valley footage": "Bhote Koshi 河谷画面", "Flood footage": "洪水画面", "Compilation and casualty claims": "视频合集与伤亡说法", - "SOURCE GEOCONFIRMED · WORLD-PINNED": "来源已地理核实 · 已标定于地图", + "SOURCE GEOCONFIRMED · WORLD-PINNED": "来源位置已核实 · 已在地图上标注", "SOURCE CREDITED · LOCATION UNRESOLVED": "来源已署名 · 位置未确定", "REFERENCE ONLY · CLAIMS UNVERIFIED": "仅供参考 · 说法未核实", - "SOURCE GEOCONFIRMED": "来源已地理核实", + "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": "崩塌观察点", @@ -98,7 +98,7 @@ "YOUTUBE {state} · OPEN ORIGINAL": "YOUTUBE {state} · 打开原始来源", "INITIATION": "起始", "DAMMING": "堰塞", - "SECONDARY FAILURE": "二次溃决", + "SECONDARY FAILURE": "第二次滑坡", "BORDER ARRIVAL": "抵达边境", "PROPAGATION": "演进", "DOWNSTREAM PASSAGE": "向下游推进", @@ -125,11 +125,11 @@ "Historical reference and post-event image split": "历史参考与灾后影像的分割", "RECONSTRUCTION CLOCK": "重构时钟", "Schematic downstream progression": "下游演进示意", - "Story beat navigation": "故事节拍导航", - "Previous story beat": "上一个故事节拍", + "Story beat navigation": "事件节点导航", + "Previous story beat": "上一个事件节点", "CAUSE": "成因", "CAPTURE TIME UNVERIFIED": "拍摄时间未核实", - "Next story beat": "下一个故事节拍", + "Next story beat": "下一个事件节点", "▶ PLAY": "▶ 播放", "Play the next shot and continue through this scene": "播放下一个镜头并继续整个场景", "▶ PLAY SCENE": "▶ 播放场景", @@ -300,7 +300,7 @@ "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.": "它像一座禁地座舱——然后你会发现,数据来源都是公开的,数据都是真实的。", + "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.": "此浏览器阻止了存储,因此无法保存。", @@ -338,7 +338,7 @@ "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}", + "NEAR {poi} ({city}) {distance}": "{poi}({city})附近 {distance}", "SECTOR {lat} {lon}": "扇区 {lat} {lon}", "N": "北", "S": "南", @@ -349,7 +349,7 @@ }, "POWERED UP": "已增强", "Provider Settings does not answer proxied requests": "服务商设置不响应经代理的请求", - "Provider Settings is disabled while sharing is enabled": "启用共享时,服务商设置不可用", + "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", @@ -371,7 +371,7 @@ "{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}存储", + "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 登录状态,请重试。", @@ -450,7 +450,7 @@ "SHOW NEAREST": "最近摄像头", "Stop following the current object before navigating to a camera": "请先停止跟踪当前目标,再前往摄像头", "Move to the nearest loaded camera and show its details": "前往最近已加载的摄像头并显示详情", - "Camera badges": "摄像头徽标", + "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} 个" @@ -526,7 +526,7 @@ "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.": "覆盖大西洋及北太平洋东部/中部;不含全球气旋。", + "Atlantic and eastern/central North Pacific; not worldwide cyclone coverage.": "仅覆盖大西洋及北太平洋东部和中部,并非全球气旋覆盖。", "Cyclone advisories unavailable": "气旋公告不可用", "Cyclone data unavailable": "气旋数据不可用", "Potential tropical cyclone": "潜在热带气旋", @@ -535,9 +535,9 @@ "Tropical depression": "热带低压", "Subtropical storm": "亚热带风暴", "Subtropical depression": "亚热带低压", - "Track and cone match this advisory": "路径和锥体与本期公告一致", - "Track/cone awaiting advisory {number}": "路径/锥体等待第 {number} 号公告", - "Track/cone unavailable": "路径/锥体不可用", + "Track and cone match this advisory": "路径和预报不确定范围与本期公告一致", + "Track/cone awaiting advisory {number}": "路径/预报不确定范围等待第 {number} 号公告", + "Track/cone unavailable": "路径/预报不确定范围不可用", "Cached advisory · stale source": "缓存公告 · 数据源已过期", "Loading advisories…": "正在加载公告…", "{count} active storms": { @@ -554,9 +554,9 @@ "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 公告。锥体描述的是预报中心路径的不确定范围,不代表气旋尺寸或全部灾害区域。预报点标注的是数据源的前导小时数,不是按公告发布时刻推算的时间。几何图形贴地显示;显示高度并非天气高度。请以官方公告为准。", + "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": "步行", @@ -658,15 +658,15 @@ "FLY ···": "飞行 ···", "FLYING": "飞行中", "FLY": "飞行", - "Fly the camera along the route": "沿路线飞行相机", + "Fly the camera along the route": "让相机沿路线飞行", "Place A and B first": "请先放置 A 和 B", "action|CLEAR": "清除", - "Remove the route and both markers": "移除路线和两个标记点", + "Remove the route and both markers": "移除路线和两个标注点", "press Escape twice to leave Draw": "按两次 Escape 退出“绘制”", "{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": "逐向导航路线", + "Turn-by-turn directions": "分步导航", "Another map tool is using clicks — close it, then SET A again": "另一个地图工具正在占用点击——请先关闭它,再重新设置 A", "Routing…": "正在规划路线…", "No route found": "未找到路线", @@ -706,7 +706,7 @@ "fire confidence|high": "高", "fire confidence|nominal": "中", "fire confidence|low": "低", - "age|<1h": "<1 小时", + "age|<1h": "不到 1 小时", "age|{count}h": "{count} 小时", "age|{count}d": "{count} 天", "Fire · FRP {frp} MW": "火点 · FRP {frp} MW", @@ -759,17 +759,17 @@ "LIFTOFF · {mission}": "点火升空 · {mission}", "ASCENT REPLAY · {mission}": "上升回放 · {mission}", "ASCENT ESTIMATE · {mission}": "上升估算 · {mission}", - "STAGE RE-ENTRY / RECOVERY · {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", + "{value} KM DOWNRANGE": "航程 {value} km", "POSITION UNAVAILABLE": "位置不可用", "FLIGHT {count}": "第 {count} 次飞行", "REUSED": "复用", - "NO STAGE RE-ENTRY / RECOVERY DATA": "无级段再入 / 回收数据", + "NO STAGE RE-ENTRY / RECOVERY DATA": "无子级再入 / 回收数据", "{count} / 30D": "{count} / 30 天", "NO MISSIONS AVAILABLE IN THE CURRENT 30-DAY WINDOW": "当前 30 天窗口内没有可用任务", "DATE UNAVAILABLE": "日期不可用", @@ -793,8 +793,8 @@ "NAME": "名称", "TYPE": "类型", "DESTINATION": "目的地", - "STAGE / RE-ENTRY / RECOVERY": "级段 / 再入 / 回收", - "STAGE": "级段", + "STAGE / RE-ENTRY / RECOVERY": "子级 / 再入 / 回收", + "STAGE": "子级", "STATUS": "状态", "FINAL POSITION": "最终位置", "REPLAY SPEED": "回放速度", @@ -806,7 +806,7 @@ "SHOW ALL / DESELECT": "显示全部 / 取消选择", "Pause replay": "暂停回放", "Cancel replay": "取消回放", - "EST. DOWNRANGE": "预估射程", + "EST. DOWNRANGE": "预估航程", "PLANNED · ": "计划 · ", "SUPPLIED TRAJECTORY POINTS": "提供的轨迹点", "RECONSTRUCTED ESTIMATE": "重建估算", @@ -828,9 +828,9 @@ "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} FPM", + "ALT {altitude} FT · GS {speed} KT · TRK {track}": "高度 {altitude} ft · 地速 {speed} kn · 航迹角 {track}", + "V/S {rate}": "升降率 {rate}", + "V/S {rate} FPM": "升降率 {rate} ft/min", "POSITION {age}": "位置 {age}", "POSITION {seconds} S AGO": "位置 {seconds} 秒前", "{count} MSGS": { @@ -938,21 +938,21 @@ "World": "世界音乐", "overview": "概览", "cloud unknown": "云量未知", - "{percent}% scene cloud": "场景云量 {percent}%", - "{min}–{max}% scene cloud": "场景云量 {min}–{max}%", + "{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": "目录已截断 · 可能还有更新的晴天", + "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": "Sentinel-2 与 Landsat,经 HLS,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 以下几乎没有细节", + "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", @@ -1023,15 +1023,15 @@ "Panoramax sent too much data": "Panoramax 返回的数据过多", "Panoramax is busy": "Panoramax 繁忙", "Panoramax sent an unreadable answer": "Panoramax 返回的响应无法读取", - "No street-level imagery providers configured": "未配置街景影像提供商", + "No street-level imagery providers configured": "未配置街景影像服务商", "{names}: by upload date": "{names}:按上传日期", "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 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": "本地路面仍在加载", @@ -1041,8 +1041,8 @@ "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 密钥)", + "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 每日配额", @@ -1051,7 +1051,7 @@ "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} · No flow roads in view": "实时 · 道路:{source} · 视野内没有带路况数据的道路", "LIVE · Roads: {source} · Flow {percent}%": "实时 · 道路:{source} · 路况 {percent}%", "SIMULATED · Roads: {source} · Flow {percent}%": "模拟 · 道路:{source} · 路况 {percent}%", "Syncing flow · Roads: OpenStreetMap · Flow: TomTom · Unmatched: simulated": "正在同步路况 · 道路:OpenStreetMap · 路况:TomTom · 未匹配:模拟", @@ -1073,7 +1073,7 @@ "occupancy|not boardable": "无法上车", "Route {route}": "线路 {route}", "Vehicle {vehicle}": "车辆 {vehicle}", - "vehicle motion|Stopped": "停驶", + "vehicle motion|Stopped": "已停车", "No recent fix": "无近期定位", "Waiting for update": "等待更新", "Moving": "行驶中", @@ -1147,7 +1147,7 @@ "frame {index}/{total}": "第 {index}/{total} 帧", "loading": "加载中", "Observation: unavailable": "观测:不可用", - "Contiguous US · gaps ≠ no rain": "美国本土 · 无回波 ≠ 无降雨", + "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 小时", @@ -1167,7 +1167,7 @@ "strikes/km²/min ×10³": "次闪击/km²/分钟 ×10³", "N. America": "北美", "Global": "全球", - "Hourly global mosaic; usually 2–3 hours delayed": "每小时全球拼图;通常延迟 2–3 小时", + "Hourly global mosaic; usually 2–3 hours delayed": "每小时全球镶嵌图;通常延迟 2–3 小时", "GOES regional clouds; approximately 5-minute updates": "GOES 区域云图;约 5 分钟更新", "Clouds only": "仅云区", "Apply the full cloud brightness key; 100% filter strength": "应用完整的云层亮度映射;滤镜强度 100%", @@ -1185,7 +1185,7 @@ "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.": "NOAA/NWS 15 分钟闪电密度,来源为 Vaisala NLDN/GLD360。覆盖自 110°E 经太平洋/美洲至 0°,25°S–80°N。不是实时闪击计数、不是全球覆盖,也不构成安全警示。", "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 全球长波拼图。“仅云区”会调暗除明亮冷云顶之外的一切;这是亮度滤镜,不是云掩膜。覆盖范围与新鲜度因区域而异。", + "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": "滤镜", @@ -1197,7 +1197,7 @@ "Some weather tiles unavailable": "部分天气瓦片不可用", "Malformed weather manifest": "天气清单格式错误", "Weather request timed out": "天气请求超时", - "Temperature field unavailable": "温度字段不可用", + "Temperature field unavailable": "温度场不可用", "Mean sea level pressure field unavailable": "平均海平面气压场不可用", "Wind upstream unavailable": "风场上游数据源不可用", "{source} HTTP {status}": "{source} HTTP {status}", @@ -1212,7 +1212,7 @@ "{model} forecast · {time}": "{model} 预报 · {time}", "Loading forecast": "正在加载预报", "Preparing flow": "正在准备流场", - "Selected field unavailable": "所选字段不可用", + "Selected field unavailable": "所选气象场不可用", "Cached forecast · stale": "缓存预报 · 已过期", "ECMWF IFS surface forecast": "ECMWF IFS 地面预报", "NOAA GFS surface forecast": "NOAA GFS 地面预报", @@ -1224,22 +1224,22 @@ "Speed": "风速", "Pressure": "气压", "Temperature": "气温", - "Wind trails with no field shading": "仅风流线,不叠加字段填色", + "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": "地面以上两米的气温", "Wind speed units": "风速单位", - "Read wind at map center": "读取地图中心风速", + "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": "所选字段不可用 · 风场仍可见", + "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;显示高度并非天气高度。视图光照仅为可读性服务。动画展示的是单一固定预报内的流场,不推进时间。颜色字段贴附于球体底图或当前的照片级 3D Tiles。", - "MODEL": "模型", - "FIELD": "字段", + "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;显示高度并非天气高度。视图光照仅为可读性服务。动画展示的是单一固定预报内的流场,不推进时间。气象场填色叠加于地球底图或当前的照片级 3D Tiles。", + "MODEL": "模式", + "FIELD": "要素", "UNITS": "单位", "MOTION": "运动", "Forecast": "预报", @@ -1265,7 +1265,7 @@ "{model} · valid {time}": "{model} · 有效时间 {time}", "Wind speed field unavailable": "风速场不可用", "Globe imagery unavailable": "球体影像不可用", - "Globe field image unavailable": "球体字段图像不可用", + "Globe field image unavailable": "地球气象场图像不可用", "Unknown wind model": "未知风场模式", "Unknown weather overlay": "未知天气叠加层", "Wind request timed out": "风场请求超时", @@ -1292,11 +1292,11 @@ "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": "请先退出座舱,再取景目标", + "Exit cockpit to frame contacts": "请先退出座舱,再将目标纳入视野", "Visible map targets": "可见的地图目标", "Focusing {label}": "正在聚焦 {label}", "Source": "来源", - "Camera ownership changed": "相机所有权已变更", + "Camera ownership changed": "相机控制权已变更", "Saved project could not be read; storage preserved. Import a valid file to resume saving.": "已保存的项目无法读取;存储已保留。请导入有效文件以恢复保存。", "Ready": "就绪", "Scene layer turned off": "场景图层已关闭", @@ -1306,7 +1306,7 @@ "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}: evidence beats changed": "无法更新 {scene}:证据节点已变化", "Cannot update {scene}: shot bindings are incomplete": "无法更新 {scene}:镜头绑定不完整", "Updated {count} shots: {scene}": { "other": "已更新 {count} 个镜头:{scene}" @@ -1314,8 +1314,8 @@ "Appended {count} shots: {scene}": { "other": "已追加 {count} 个镜头:{scene}" }, - "Cannot capture shot: camera not ready": "无法截取镜头:相机未就绪", - "Captured: {scene} / {shot}": "已截取:{scene} / {shot}", + "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}", @@ -1383,23 +1383,23 @@ "{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.": "在你应用此文件之前,不会载入或更改任何内容。", + "{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.": "导入未应用。当前项目可能已发生变化。", + "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.": "相机位置使用度与椭球面上的米数。草稿在替换已保存场景之前会先进行校验。", + "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": "将当前相机采集为锚点", + "Capture camera as anchor": "将当前相机位置设为锚点", "Anchor added to draft": "锚点已加入草稿", - "Set move start to current camera": "将运动起点设为当前相机", - "Move added to draft; destination remains the shot camera": "运动已加入草稿;终点仍为镜头相机", + "Set move start to current camera": "将运镜起点设为当前相机位置", + "Move added to draft; destination remains the shot camera": "运镜已加入草稿;终点仍为该镜头的相机位置", "Use ordinary flight": "使用普通飞行", "Invalid shot JSON": "镜头 JSON 无效", "Apply details": "应用详情", @@ -1454,7 +1454,7 @@ "Vignette": "暗角", "Pixelation": "像素化", "Distortion": "畸变", - "Instability": "不稳定", + "Instability": "抖动", "Density": "密度", "Gain": "增益", "Bloom": "泛光", @@ -1475,10 +1475,10 @@ "Copy failed": "复制失败", "Return map to straight-down view": "将地图恢复为垂直俯视", "Tilt map to oblique view": "将地图倾斜至斜视角", - "Reset map to north up. Current heading {heading} degrees": "将地图重置为朝北。当前航向 {heading} 度", + "Reset map to north up. Current heading {heading} degrees": "将地图重置为正北朝上。当前方位 {heading} 度", "Tilted view": "倾斜视角", "Straight-down view": "垂直俯视", - "North up": "朝北", + "North up": "正北朝上", "CCTV calibration saved": "CCTV 校准已保存", "HDG": "HDG", "PITCH": "俯仰", @@ -1496,7 +1496,7 @@ "UPSTREAM": "上游", "SYNTHETIC": "合成", "CONFIGURED": "已配置", - "SEED": "种子", + "SEED": "预置", "UNKNOWN": "未知", "ERROR": "错误", "Live HLS connected": "实时 HLS 已连接", @@ -1573,10 +1573,10 @@ "CLOUD UNKNOWN": "云量未知", "{source} · LOCATION QUERY": "{source} · 位置查询", "REGIONAL NEWS": "地区新闻", - "{label} · {radius} AIR/SEA WINDOW": "{label} · {radius} 空中 / 海上范围", + "{label} · {radius} AIR/SEA WINDOW": "{label} · {radius} 航班 / 船舶范围", "{scope} · INSTALLATIONS {coverage}": "{scope} · 设施 {coverage}", "CONTEXT STANDBY": "态势待机", - "ENABLE GLOBAL CONTEXT FOR PROXIMITY PINGS": "启用全局态势以获取邻近提示", + "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": "目标已脱离其数据源 · 读数保持最后已知值", @@ -1592,7 +1592,7 @@ }, "AVAILABLE INPUTS CURRENT · NOT AN ALL-CLEAR": "可用输入均为最新 · 不代表安全无虞", "SQUAWK {code} · {status}": "应答码 {code} · {status}", - "ADS-B PRIORITY · {status}": "ADS-B 优先 · {status}", + "ADS-B PRIORITY · {status}": "ADS-B 优先级 · {status}", "ADS-B EMERGENCY · {status}": "ADS-B 紧急 · {status}", "AIRCRAFT": "飞机", "ACQUIRING SURFACE": "正在获取地面数据", @@ -1614,7 +1614,7 @@ "Anime": "动漫", "Noir": "黑白", "Snow": "雪景", - "Current cockpit vision style: {style}. Activate for next style.": "当前座舱视觉样式:{style}。激活可切换到下一个样式。", + "Current cockpit vision style: {style}. Activate for next style.": "当前座舱视觉样式:{style}。点击可切换到下一个样式。", "Current style: {style} — click for next": "当前样式:{style}——点击切换下一个", "Collapse Contact panel": "收起目标面板", "Expand Contact panel": "展开目标面板", @@ -1624,9 +1624,9 @@ "Expand cockpit briefing panel": "展开座舱简报面板", "Collapse briefing panel": "收起简报面板", "Expand briefing panel": "展开简报面板", - "OBSERVED / MAPPED PINGS": "观测 / 地图标注提示", + "OBSERVED / MAPPED PINGS": "观测 / 地图标注目标", "SOURCE-BACKED EVENTS · NO SYNTHETIC NEWS": "来源可查的事件 · 无合成新闻", - "LATEST LOCATION-MATCHED REPORTING": "最新的位置匹配报道", + "LATEST LOCATION-MATCHED REPORTING": "当前地区最新报道", "{source} · LOCATION QUERY · RECENT": "{source} · 位置查询 · 近期", "LOCAL INFO": "本地信息", "PLACE / CONDITIONS / POSITION": "地点 / 状况 / 位置", @@ -1775,7 +1775,7 @@ "closed": "已关闭", "feed status|live": "实时", "feed status|stale": "过期", - "feed status|unreachable": "无法访问", + "feed status|unreachable": "无法连接", "feed status|invalid": "无效", "waiting for IQ": "等待 IQ 数据", "{count} messages": { @@ -1789,7 +1789,7 @@ "audio signal --": "音频信号 --", "audio {state}": "音频 {state}", "{count} HEARD": { - "other": "已收到 {count} 架" + "other": "已接收 {count} 架飞机的信号" }, "DISCONNECT": "断开连接", "CONNECT": "连接", @@ -1817,8 +1817,8 @@ "Resetting cockpit to full globe view": "正在将座舱重置为完整地球视图", "MAP": "地图", "panel": "面板", - "Expand {name}": "展开{name}", - "Collapse {name}": "收起{name}", + "Expand {name}": "展开 {name}", + "Collapse {name}": "收起 {name}", "Expand Radio": "展开电台", "Collapse Radio": "收起电台", "Expand Radio section": "展开电台区域", @@ -1863,7 +1863,7 @@ "NO STATION SELECTED": "未选择电台", "Directory metadata only": "仅目录元数据", "Loading station directory…": "正在加载电台目录…", - "Choose a globe marker or use next.": "请选择地球上的标记或使用“下一个”。", + "Choose a globe marker or use next.": "请选择地球上的标注或使用“下一个”。", "TAGS · {tags}": "标签 · {tags}", "Play": "播放", "PAUSE": "暂停", @@ -1882,7 +1882,7 @@ " · degraded directory": " · 目录降级", " · stale directory": " · 目录过期", " · outside current filter": " · 不在当前筛选范围内", - "Ready — playback starts only from your action": "就绪——仅在你操作后才开始播放", + "Ready — playback starts only from your action": "就绪——手动操作后开始播放", "Radio off": "电台已关闭", "Connecting directly to broadcaster…": "正在直接连接广播方…", "Buffering broadcaster stream…": "正在缓冲广播流…", @@ -1904,7 +1904,7 @@ "Latest": "最新", "Newest frame per product": "每个产品的最新一帧", "Replay observed history": "回放观测历史", - "Imagery on Esri · Google 3D returns when cleared": "影像来自 Esri · 清除后恢复 Google 3D", + "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}°", @@ -1913,11 +1913,11 @@ "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 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 clear day": "最近少云日期", "newest day (cloudy)": "最新一天(多云)", "newest day (partial coverage)": "最新一天(部分覆盖)", "newest overview": "最新概览", @@ -1939,13 +1939,13 @@ "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 显示聚焦日", + "← → 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 钉住", + "Imagery days, newest first · arrows move, S or A and B pin": "影像日期(最新在前)· 方向键移动,S、A、B 固定", "On the map": "地图上", "Mode": "模式", "Imagery opacity": "影像不透明度", @@ -1961,22 +1961,22 @@ "PREVIEW": "预览", "Newest day with imagery for this box": "此框选有影像的最新一天", "SHOW": "显示", - "Pin {day} as {slot}": "将 {day} 钉为 {slot}", + "Pin {day} as {slot}": "将 {day} 固定为 {slot}", "Show {day}": "显示 {day}", - "pinned {slot}": "已钉为 {slot}", + "pinned {slot}": "已固定为 {slot}", "shown": "显示中", "preview": "预览", "start here": "从此开始", "No box": "无框选", "Searching": "搜索中", "Every day is empty here": "此处每天都为空", - "Sources off": "数据源未开启", + "Sources off": "数据源已关闭", "VS": "对比", "Not set": "未设置", "Basemap": "底图", - "Source off": "数据源已关", - "Unpin {slot}": "取消钉住 {slot}", - "Unpin the image": "取消钉住图像", + "Source off": "数据源已关闭", + "Unpin {slot}": "取消固定 {slot}", + "Unpin the image": "取消固定图像", "EXPORT {slot}": "导出 {slot}", "EXPORT": "导出", "Download {slot} as a PNG": "将 {slot} 下载为 PNG", @@ -2007,7 +2007,7 @@ "Stopped (Esc)": "已停止(Esc)", "SPARSE": "稀疏", "BALANCED": "均衡", - "No shots yet. Use CAPTURE SHOT to save current look.": "暂无镜头。使用“截取镜头”保存当前画面。", + "No shots yet. Use CAPTURE SHOT to save current look.": "暂无镜头。使用“保存镜头”保存当前视角及设置。", "Double-click to rename": "双击重命名", "Shot name": "镜头名称", "LOAD": "载入", @@ -2021,21 +2021,21 @@ "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": "共享的卫星不可用", + "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} 不可用", + "Shared {subject} follow expired": "分享的 {subject} 跟随已过期", + "Shared {subject} could not be restored — feed unavailable": "无法恢复分享的 {subject}——数据源不可用", + "Shared {subject} is unavailable": "分享的 {subject} 不可用", "ACQUIRING": "获取中", - "SHARED {subject}": "共享 {subject}", + "SHARED {subject}": "分享 {subject}", "subject": "目标", "LOADING LIVE DATA": "正在加载实时数据", "LOAD COMPLETE": "加载完成", @@ -2093,7 +2093,7 @@ }, "Loading {feeds}": "正在加载 {feeds}", "{count} heard": { - "other": "已收到 {count} 架" + "other": "已接收 {count} 架飞机的信号" }, "USB {rate} msg/s": "USB {rate} msg/s", "{rate} msg/s": "{rate} msg/s", @@ -2161,21 +2161,21 @@ "Detection overlay: off": "探测叠加层:关闭", "DETECT": "探测", "{time} · {minutes} min ago": "{time} · {minutes} 分钟前", - "Active weather": "活跃天气", + "Active weather": "已启用的天气图层", "LATEST · newest per product": "最新 · 每个产品的最新一帧", - "Forecast · valid {valid} · issued {issued}": "预报 · 生效 {valid} · 发布 {issued}", + "Forecast · valid {valid} · issued {issued}": "预报 · 有效时间 {valid} · 发布 {issued}", "{detail} · Does not follow history": "{detail} · 不随观测历史变化", - "Forecast · valid {valid}": "预报 · 生效 {valid}", + "Forecast · valid {valid}": "预报 · 有效时间 {valid}", "voice|OFF": "关", "voice|CONNECTING": "连接中", "voice|LISTENING": "聆听中", "voice|EXECUTING": "执行中", "voice|ERROR": "错误", - "AI AGENT": "AI 代理", + "AI AGENT": "AI 智能体", "Voice model tier — applies next session": "语音模型档位——下次会话生效", "STD": "标准", "Estimated session cost": "预估会话费用", - "Voice control — activate to toggle voice; hold Space to speak": "语音控制——激活可切换语音;按住空格键说话", + "Voice control — activate to toggle voice; hold Space to speak": "语音控制——点击可切换语音;按住空格键说话", "ON/OFF": "开/关", "VOICE STANDBY": "语音待机", "VOICE CONTROL": "语音控制", @@ -2183,16 +2183,16 @@ "Voice assistant": "语音助手", "VOICE": "语音", "Dismiss voice card": "关闭语音卡片", - "YOU": "用户", - "THIS": "目标", + "YOU": "你", + "THIS": "所指", "Plan": "计划", "Referenced items": "引用条目", - "NOTES & SOURCES": "笔记与来源", + "NOTES & SOURCES": "说明与来源", "VOICE SYSTEM ERROR": "语音系统错误", "DISMISS": "关闭", "Check microphone permission and network access, then try again.": "请检查麦克风权限和网络连接,然后重试。", "Stopped by voice": "已通过语音停止", - "WebRTC microphone support unavailable": "不支持 WebRTC 麦克风", + "WebRTC microphone support unavailable": "当前浏览器不支持通过 WebRTC 使用麦克风", "Requesting microphone": "正在请求麦克风", "Release Space to send": "松开空格键发送", "Hold Space to talk": "按住空格键说话", @@ -2201,11 +2201,11 @@ "VOICE UNAVAILABLE": "语音不可用", "VOICE ACTIVE": "语音已启用", "Voice session could not be started.": "无法启动语音会话。", - "MINI": "迷你", + "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": "迷你", + "mini": "轻量", "COST UNKNOWN": "费用未知", "ChatGPT OAuth session: {responses} response(s), {input} input and {output} output tokens, {captions} caption transcription(s) reported.": "ChatGPT OAuth 会话:已报告 {responses} 个响应、{input} 个输入 token、{output} 个输出 token、{captions} 次字幕转写。", "USD cost and the API spend cap are unavailable for this auth mode.": "此认证模式下无法提供美元费用与 API 消费上限。", @@ -2300,7 +2300,7 @@ "partial": "部分", "fallback": "备用", "unavailable": "不可用", - "Not answered: {layers}": "未回答:{layers}", + "Not answered: {layers}": "未响应:{layers}", "Toggle voice control": "切换语音控制", "Activate to toggle voice": "点击切换语音", "Voice active": "语音已启用", @@ -2310,7 +2310,7 @@ "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-live-vessels 图层未开启", + "The ais-live-vessels layer is not enabled": "实时 AIS 船舶图层未开启", "Could not enable the requested layer": "无法开启所请求的图层", "Could not disable the requested layer": "无法关闭所请求的图层", "Radio layer unavailable": "电台图层不可用", @@ -2334,11 +2334,11 @@ "Unknown target layer: {target}": "未知目标图层:{target}", "nominal": "正常", "{label} on": "{label} 已开启", - "Frame aircraft": "取景飞机", - "Frame military aircraft": "取景军用飞机", - "Frame ships": "取景船舶", - "Frame satellites": "取景卫星", - "Frame contacts": "取景目标", + "Frame aircraft": "将飞机纳入视野", + "Frame military aircraft": "将军用飞机纳入视野", + "Frame ships": "将船舶纳入视野", + "Frame satellites": "将卫星纳入视野", + "Frame contacts": "将目标纳入视野", "{count} aircraft in frame": { "other": "画面中 {count} 架飞机" }, @@ -2358,13 +2358,13 @@ "Nearest ships": "最近的船舶", "Nearest satellites": "最近的卫星", "Nearest aircraft": "最近的飞机", - "{first} and {second}": "{first}和{second}", + "{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 个目标", + "Framing uses the nearest 80 loaded contacts": "调整视野时仅包含已加载的最近 80 个目标", "{distance} from {location}": "距 {location} {distance}", "Altitude {altitude}": "高度 {altitude}", "{origin} to {destination}": "{origin} 至 {destination}", @@ -2373,15 +2373,15 @@ "Registration {registration}": "注册号 {registration}", "View state": "视野状态", "Marked {count} places": { - "other": "已标记 {count} 个地点" + "other": "已标注 {count} 个地点" }, - "Nothing marked": "没有标记", + "Nothing marked": "没有标注", "Not found: {places}": "未找到:{places}", - "Tracing outlines": "正在描画边界", - "Frame flights": "取景航班", - "Frame military": "取景军用航班", - "Frame vessels": "取景船舶", - "Frame {target}": "取景 {target}", + "Tracing outlines": "正在绘制轮廓", + "Frame flights": "将航班纳入视野", + "Frame military": "将军用航班纳入视野", + "Frame vessels": "将船舶纳入视野", + "Frame {target}": "将 {target} 纳入视野", "Zoom out to globe": "缩小至地球视图", "Open data layers": "打开数据图层", "Open or close a panel": "打开或关闭面板", @@ -2419,7 +2419,7 @@ "Check view state": "检查视野状态", "Action": "操作", "Finding places": "正在查找地点", - "Tracing outline": "正在描画边界", + "Tracing outline": "正在绘制轮廓", "Looking up place": "正在查询地点", "Flying": "飞行中", "Turning on layer": "正在开启图层", @@ -2430,8 +2430,8 @@ "done": "完成", "failed": "失败", "cancelled": "已取消", - "Outline traced": "边界描画完成", - "Outline unavailable": "边界不可用", + "Outline traced": "轮廓已绘制", + "Outline unavailable": "轮廓不可用", "Source: {label}": "来源:{label}", "WORKING": "处理中", "DONE": "完成", @@ -2449,7 +2449,7 @@ "Cockpit vision style": "座舱视觉样式", "Previous cockpit vision style": "上一个座舱视觉样式", "Previous vision style": "上一个视觉样式", - "Current cockpit vision style: NORMAL. Activate for next style.": "当前座舱视觉样式:常规。激活可切换到下一个样式。", + "Current cockpit vision style: NORMAL. Activate for next style.": "当前座舱视觉样式:常规。点击可切换到下一个样式。", "Current style: NORMAL — click for next": "当前样式:常规——点击切换下一个", "CURRENT": "当前", "Next cockpit vision style": "下一个座舱视觉样式", @@ -2534,7 +2534,7 @@ "Search location by name or coordinates": "按名称或坐标搜索位置", "Search any location...": "搜索任意位置…", "WEATHER": "天气", - "Active weather products": "活跃天气产品", + "Active weather products": "已启用的天气产品", "RECENT IMAGERY": "近期影像", "Imagery days in the box": "框选内的影像日期", "CONTEXT": "态势", @@ -2564,7 +2564,7 @@ "STATION TAG": "电台标签", "Filter stations by station tag": "按电台标签筛选电台", "All": "全部", - "Enable Radio, then choose a globe marker or use next.": "请先启用电台,然后选择地球上的标记或使用“下一个”。", + "Enable Radio, then choose a globe marker or use next.": "请先启用电台,然后选择地球上的标注或使用“下一个”。", "DRAG TO TUNE": "拖动调谐", "Tune available internet radio stations": "调谐可用的网络电台", "ALL · DRAG THE NEEDLE": "全部 · 拖动指针", @@ -2590,7 +2590,7 @@ "RTL-SDR tuner gain for the current mode": "当前模式下的 RTL-SDR 调谐器增益", "AUTO": "自动", "MSG/S": "msg/s", - "HEARD": "已收到", + "HEARD": "已接收信号", "POSITIONED": "已定位", "FM MHZ": "FM MHz", "TUNE": "调谐", @@ -2628,20 +2628,20 @@ "Weighted": "加权", "Fade": "渐隐", "Detection fade distance": "探测渐隐距离", - "World-overlay fade distance outside the keyhole as a percentage of its radius": "锁孔之外的世界叠加层渐隐距离,以其半径的百分比表示", + "World-overlay fade distance outside the keyhole as a percentage of its radius": "视域之外的世界叠加层渐隐距离,以其半径的百分比表示", "Outside": "外部", - "Detection opacity outside the keyhole": "锁孔之外的探测不透明度", + "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": "邻近", + "Proximity": "近处", "Scope — the circular viewport mask": "视域——圆形视口遮罩", "Scope": "视域", "Feather": "羽化", "Scope edge feather": "视域边缘羽化", - "Scope edge feather as a percentage of the keyhole radius": "视域边缘羽化,以锁孔半径的百分比表示", + "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": "要绘制的形状", @@ -2657,7 +2657,7 @@ "Green": "绿色", "Red": "红色", "Clear": "清除", - "Remove every mark from the board": "清除图板上的所有标记", + "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 文件", @@ -2670,16 +2670,16 @@ "Sharpening": "锐化", "Sharpen": "锐化", "Sharpen intensity": "锐化强度", - "EXIT CLEAN VIEW": "退出纯净视图", + "EXIT CLEAN VIEW": "退出纯净界面", "Return UI controls": "恢复界面控件", - "Initializing photorealistic world...": "正在初始化逼真世界…", + "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": "摄像头姿态——点击数值可输入", + "Camera pose — click a value to type": "摄像头位姿——点击数值可输入", "Heading (compass °) — click to type": "航向(罗盘 °)——点击输入", "Pitch (° up/down) — click to type": "俯仰(° 上/下)——点击输入", "Horizontal FOV (°) — click to type": "水平 FOV(°)——点击输入", @@ -2700,9 +2700,9 @@ "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": "提供商", + "Open this image on the provider's site": "在服务商网站上打开此图像", + "Imagery providers": "影像服务商", + "PROVIDERS": "服务商", "Imagery filters": "影像筛选", "IMAGERY": "影像", "Panorama filter": "全景筛选", @@ -2712,7 +2712,7 @@ "SCENES": "场景", "Scene recipe": "场景方案", "NEW": "新建", - "CAPTURE SHOT": "截取镜头", + "CAPTURE SHOT": "保存镜头", "UPDATE SHOT": "更新镜头", "START": "启动", "EXPORT PRESETS": "导出预设", @@ -2725,26 +2725,26 @@ "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 密钥提供逼真的地球,其余数据源都叠加在其上。", + "The Google Maps key buys the photorealistic planet — everything else stacks on top.": "Google Maps 密钥提供照片级 3D 地球,其余数据源都叠加在其上。", "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 to north up": "将地图重置为正北朝上", "Reset map bearing to north": "重置地图方位为正北", "Reset camera and return to full globe view": "重置相机并返回完整地球视图", "MISSION CONTROL · FIRST LAUNCH": "任务控制 · 首次启动", - "Choose your first view": "选择你的首个视图", + "Choose your first view": "选择初始视图", "LIVE CONTACTS": "实时目标", "Aircraft, vessels and nearby intelligence": "飞机、船舶及周边情报", "Launches, spacecraft and orbital context": "发射、航天器与轨道态势", "EXPLORE MANUALLY": "手动探索", - "Begin with a clean globe": "从干净的地球开始", + "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.": "提示:停靠栏中的 GEV 麦克风按钮可让你用语音操控地图。", + "Tip: the GEV MIC button in the dock lets you talk to the map.": "提示:可通过停靠栏中的 GEV 麦克风按钮用语音操控地图。", "hemisphere|N": "N", "hemisphere|S": "S", "hemisphere|E": "E", From fd78bc38845c7fbc6fc8bd82c6eac1e6b0048155 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Wed, 21 Oct 2026 13:36:38 +0400 Subject: [PATCH 20/25] fix(zh-CN): localize readouts and keep short labels intact Preserve Chinese unit casing and spacing in the HUD and cockpit, localize FIRMS cache ages, CCTV health and radio counts, and keep imagery resolution and route modes readable. Add regression coverage for localized output and unchanged English. Co-Authored-By: Codex (GPT-6) --- src/hudLocalityFormat.js | 4 +- src/i18n/locales/es.json | 4 +- src/i18n/locales/ru.json | 4 +- src/i18n/locales/zh-CN.json | 4 +- src/i18n/same-as-english.json | 1 + src/i18n/zh-CN-display.test.mjs | 106 ++++++++++++++++++++++++++++ src/layers/firms/model.js | 1 + src/layers/firms/queries.js | 3 +- src/layers/recentImagery/display.js | 12 +++- src/ui/cctvFrames.js | 3 +- src/ui/cockpitContext.js | 4 +- src/ui/cockpitPresentation.js | 7 +- src/ui/radioPresentation.js | 11 +-- src/ui/styles/cjk.css | 25 +++++++ 14 files changed, 171 insertions(+), 18 deletions(-) create mode 100644 src/i18n/zh-CN-display.test.mjs diff --git a/src/hudLocalityFormat.js b/src/hudLocalityFormat.js index 84278564b..b508f8d59 100644 --- a/src/hudLocalityFormat.js +++ b/src/hudLocalityFormat.js @@ -15,7 +15,9 @@ export function hudQuantity(value, unit, digits = 0) { minimumFractionDigits: digits, maximumFractionDigits: digits, }); - return `${number}${unitLabel(unit).toUpperCase()}`; + const label = unitLabel(unit); + if (getLocale() === 'zh-CN') return label ? `${number} ${label}` : number; + return `${number}${label.toUpperCase()}`; } /** One hemisphere-tagged coordinate, e.g. `21.33N` / `21,33С`. */ diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index cbba42e0e..c26430fe7 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -2941,5 +2941,7 @@ "hemisphere|N": "N", "hemisphere|S": "S", "hemisphere|E": "E", - "hemisphere|W": "O" + "hemisphere|W": "O", + "OK": "OK", + "{category} ({count})": "{category} ({count})" } diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index dcd6ef20b..dadea1a42 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -3008,5 +3008,7 @@ "hemisphere|N": "С", "hemisphere|S": "Ю", "hemisphere|E": "В", - "hemisphere|W": "З" + "hemisphere|W": "З", + "OK": "НОРМА", + "{category} ({count})": "{category} ({count})" } diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 8b5240e12..6b815f545 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -2807,5 +2807,7 @@ "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 连接即将到期。请重新启动语音以开始新对话。", "Gemini voice sessions have a time limit. When a session expires, start voice again for a new conversation.": "Gemini 语音会话有时长限制。会话到期后,请重新启动语音以开始新对话。", - "Show the street-level view full screen (Esc shrinks it)": "全屏显示街景视图(按 Esc 缩小)" + "Show the street-level view full screen (Esc shrinks it)": "全屏显示街景视图(按 Esc 缩小)", + "OK": "正常", + "{category} ({count})": "{category}({count})" } diff --git a/src/i18n/same-as-english.json b/src/i18n/same-as-english.json index 821b154b9..c331cd070 100644 --- a/src/i18n/same-as-english.json +++ b/src/i18n/same-as-english.json @@ -63,6 +63,7 @@ "NVG", "Noir", "Normal", + "OK", "POS: {time}Z", "Plan", "Pop", diff --git a/src/i18n/zh-CN-display.test.mjs b/src/i18n/zh-CN-display.test.mjs new file mode 100644 index 000000000..b9e55058d --- /dev/null +++ b/src/i18n/zh-CN-display.test.mjs @@ -0,0 +1,106 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { setLocale, t } from './index.js'; +import { hudQuantity, LOCALIZED_LOCALITY } from '../hudLocalityFormat.js'; +import { formatCockpitDistance } from '../ui/cockpitPresentation.js'; +import { cctvSourceStatusLabel } from '../ui/cctvFrames.js'; +import { READOUT_WORDS } from '../layers/recentImagery/display.js'; +import { createModel } from '../layers/firms/model.js'; +import { createQueries } from '../layers/firms/queries.js'; +import { renderRadioState } from '../ui/radioPresentation.js'; + +test('Chinese HUD and cockpit units keep their case without spacing coordinates', () => { + try { + setLocale('zh-CN'); + assert.equal(hudQuantity(0, 'km'), '0 km'); + assert.equal(hudQuantity(2069, 'km', 1), '2069.0 km'); + assert.equal(formatCockpitDistance(14000), '14\u00a0km'); + assert.equal(LOCALIZED_LOCALITY.sector({ latDeg: 21.33, lonDeg: -77.5 }), + '扇区 21.33N 77.50W'); + setLocale('en'); + assert.equal(hudQuantity(0, 'km'), '0KM'); + assert.equal(hudQuantity(2069, 'km', 1), '2069.0KM'); + assert.equal(formatCockpitDistance(14000), '14\u00a0KM'); + } finally { + setLocale('en'); + } +}); + +test('FIRMS stale cache age uses localized durations while English stays compact', () => { + const now = Date.now(); + const layerState = { _stale: true }; + const components = { + model: createModel({ services: { anchors: {} } }), + }; + const { methods } = createQueries({ layerState, components, config: {} }); + try { + for (const [hours, zh, en] of [[0.5, '不到 1 小时', '<1h'], [2, '2 小时', '2h'], [72, '3 天', '3d']]) { + layerState._lastUpdate = now - hours * 3600000; + setLocale('zh-CN'); + const expected = t('STALE · cached {age}', { age: zh }); + assert.equal(methods.getStats().loadingLabel, expected); + assert.equal(methods.getStats().error, expected); + setLocale('en'); + assert.equal(methods.getStats().loadingLabel, `STALE · cached ${en}`); + } + } finally { + setLocale('en'); + } +}); + +test('Recent Imagery spaces numeric Chinese ages and retains natural relative dates', () => { + try { + setLocale('zh-CN'); + assert.equal(READOUT_WORDS.daysAgo(9), '9 天前'); + assert.equal(READOUT_WORDS.daysAgo(0), '今天'); + assert.equal(READOUT_WORDS.daysAgo(1), '昨天'); + setLocale('en'); + assert.equal(READOUT_WORDS.daysAgo(9), '9 days ago'); + assert.equal(READOUT_WORDS.daysAgo(0), 'today'); + } finally { + setLocale('en'); + } +}); + +test('CCTV translates the healthy status and preserves unknown provider codes', () => { + try { + for (const [locale, expected] of [['zh-CN', '正常'], ['ru', 'НОРМА'], ['es', 'OK'], ['en', 'OK']]) { + setLocale(locale); + assert.equal(cctvSourceStatusLabel('ok'), expected); + assert.equal(cctvSourceStatusLabel('CUSTOM'), 'CUSTOM'); + } + } finally { + setLocale('en'); + } +}); + +test('radio option text and accessible name use the same localized parentheses', () => { + const originalDocument = globalThis.document; + const element = () => ({ + style: {}, dataset: {}, attributes: {}, + classList: { contains: () => true, toggle() {} }, + setAttribute(name, value) { this.attributes[name] = value; }, + replaceChildren(...children) { this.children = children; }, + }); + globalThis.document = { createElement: element, getElementById: () => null }; + try { + for (const [locale, label, expected] of [['zh-CN', '全部', '全部(750)'], ['en', 'All', 'All (750)']]) { + setLocale(locale); + const host = { + _radioPanel: element(), _radioFilter: element(), + _syncContextRadioLauncherState() {}, + actions: { getLifecycle: () => null, scheduleLayout() {} }, + }; + renderRadioState.call(host, { + enabled: true, categories: [{ id: 'all', label, count: 750, color: '' }], + filter: 'all', filteredCount: 750, stationCount: 750, volume: 1, + }); + const option = host._radioFilter.children[0]; + assert.equal(option.textContent, `● ${expected}`); + assert.equal(option.attributes['aria-label'], expected); + } + } finally { + globalThis.document = originalDocument; + setLocale('en'); + } +}); diff --git a/src/layers/firms/model.js b/src/layers/firms/model.js index 3a1df327b..226c32286 100644 --- a/src/layers/firms/model.js +++ b/src/layers/firms/model.js @@ -591,6 +591,7 @@ export function createModel({ formatLatLon, formatFrp, formatAge, + formatAgeLabel, formatAgoMinutes, confidenceBucket, }; diff --git a/src/layers/firms/queries.js b/src/layers/firms/queries.js index a7d22589e..c4a793f9d 100644 --- a/src/layers/firms/queries.js +++ b/src/layers/firms/queries.js @@ -64,7 +64,8 @@ export function createQueries({ const staleText = layerState._lastUpdate ? t('STALE · cached {age}', { age: - components.model.formatAge(now - layerState._lastUpdate) || '<1h', + components.model.formatAgeLabel(now - layerState._lastUpdate) || + t('<1h', { context: 'age' }), }) : t('STALE'); let loadingLabel = ''; diff --git a/src/layers/recentImagery/display.js b/src/layers/recentImagery/display.js index 5b2b5ce39..5c18d8b3f 100644 --- a/src/layers/recentImagery/display.js +++ b/src/layers/recentImagery/display.js @@ -3,7 +3,12 @@ * day's readout. The model (`model.js`) stays English and i18n-free because AI * tool services import it outside the browser; this module is page-only. */ -import { formatDate, formatRelativeTime, t } from '../../i18n/index.js'; +import { + formatDate, + formatRelativeTime, + getLocale, + t, +} from '../../i18n/index.js'; import { formatQuantity } from '../../ui/unitLabels.js'; import { dayTime } from './model.js'; @@ -26,7 +31,10 @@ export const displayMetres = (value) => formatQuantity(value, 'm'); /** The readout's words in the page's language; the model keeps them English. */ export const READOUT_WORDS = Object.freeze({ date: (day) => displayDay(day, LONG_DAY), - daysAgo: (delta) => formatRelativeTime(delta > 0 ? -delta : 0, 'day'), + daysAgo: (delta) => { + const text = formatRelativeTime(delta > 0 ? -delta : 0, 'day'); + return getLocale() === 'zh-CN' ? text.replace(/(\d)(?=天)/g, '$1 ') : text; + }, // "Sentinel-2" never breaks at its hyphen: a word joiner follows it // (JetBrains Mono has no U+2011 non-breaking hyphen). sensor: (sensor) => diff --git a/src/ui/cctvFrames.js b/src/ui/cctvFrames.js index 7976d46c8..22f4f0dce 100644 --- a/src/ui/cctvFrames.js +++ b/src/ui/cctvFrames.js @@ -19,6 +19,7 @@ const SOURCE_KIND_LABELS = Object.freeze({ }); const SOURCE_STATUS_LABELS = Object.freeze({ + ok: msg('OK'), degraded: msg('DEGRADED'), error: msg('ERROR'), unknown: msg('UNKNOWN'), @@ -51,7 +52,7 @@ export function cctvSourceKindLabel(kind) { : key.toUpperCase(); } -/** "DEGRADED" / "ПЕРЕБОИ"; OK and unknown codes stay as uppercase codes. */ +/** "DEGRADED" / "ПЕРЕБОИ"; unknown codes stay as uppercase codes. */ export function cctvSourceStatusLabel(status) { const key = String(status || 'unknown').toLowerCase(); return SOURCE_STATUS_LABELS[key] diff --git a/src/ui/cockpitContext.js b/src/ui/cockpitContext.js index 57db349ee..852cc32a8 100644 --- a/src/ui/cockpitContext.js +++ b/src/ui/cockpitContext.js @@ -1,6 +1,6 @@ /** Contact readouts and nearby-signal presentation for the Cockpit controller. */ import * as Cesium from 'cesium'; -import { formatNumber, t, tn } from '../i18n/index.js'; +import { formatNumber, getLocale, t, tn } from '../i18n/index.js'; import { unitLabel } from './unitLabels.js'; import { bearingBetweenCoordinates, @@ -15,7 +15,7 @@ const LOCALIZED_CONTEXT_SCOPE = Object.freeze({ window: ({ label, radiusKm }) => t('{label} · {radius} AIR/SEA WINDOW', { label, - radius: `${radiusKm === null ? '—' : formatNumber(radiusKm)}\u00a0${unitLabel('km').toUpperCase()}`, + radius: `${radiusKm === null ? '—' : formatNumber(radiusKm)}\u00a0${getLocale() === 'zh-CN' ? unitLabel('km') : unitLabel('km').toUpperCase()}`, }), installations: ({ scope, coverage }) => t('{scope} · INSTALLATIONS {coverage}', { scope, coverage }), diff --git a/src/ui/cockpitPresentation.js b/src/ui/cockpitPresentation.js index f8930d4e0..8285b74b3 100644 --- a/src/ui/cockpitPresentation.js +++ b/src/ui/cockpitPresentation.js @@ -1,5 +1,5 @@ /** Pure formatting and fixed presentation constants used by Cockpit components. */ -import { formatNumber, t, msg } from '../i18n/index.js'; +import { formatNumber, getLocale, t, msg } from '../i18n/index.js'; import { formatQuantity } from './unitLabels.js'; export const COCKPIT_HEADING_SLEW_DPS = 28; @@ -145,10 +145,11 @@ export function formatCockpitCoordinate(value, positive, negative) { */ export function formatCockpitDistance(distanceM) { const digits = distanceM < 10000 ? 1 : 0; - return formatQuantity(distanceM / 1000, 'km', { + const distance = formatQuantity(distanceM / 1000, 'km', { minimumFractionDigits: digits, maximumFractionDigits: digits, - }).toUpperCase(); + }); + return getLocale() === 'zh-CN' ? distance : distance.toUpperCase(); } export function formatCockpitWindDirection(value) { diff --git a/src/ui/radioPresentation.js b/src/ui/radioPresentation.js index 94431fdec..54ee379e8 100644 --- a/src/ui/radioPresentation.js +++ b/src/ui/radioPresentation.js @@ -114,13 +114,14 @@ export function renderRadioState(state) { ...state.categories.map((category) => { const option = document.createElement('option'); option.value = category.id; - option.textContent = `● ${category.label} (${category.count})`; + const label = t('{category} ({count})', { + category: category.label, + count: category.count, + }); + option.textContent = `● ${label}`; option.dataset.radioColor = category.color; option.style.color = category.color; - option.setAttribute( - 'aria-label', - `${category.label} (${category.count})`, - ); + option.setAttribute('aria-label', label); return option; }), ); diff --git a/src/ui/styles/cjk.css b/src/ui/styles/cjk.css index c44431520..9ecfa0ce3 100644 --- a/src/ui/styles/cjk.css +++ b/src/ui/styles/cjk.css @@ -27,3 +27,28 @@ html:lang(zh) .panel-layer-toggle { html:lang(zh) .pp-mode-btn { white-space: nowrap; } + +/* Keep unit symbols in their catalog case, including the cockpit readouts. */ +html:lang(zh) #intel-hud *, +html:lang(zh) #cockpit-hud * { + text-transform: none; +} + +/* Move short travel modes (步行 / 骑行) to the next line as whole words. */ +html:lang(zh) [data-layer-id='directions'] .data-toggle-meta { + word-break: keep-all; + overflow-wrap: break-word; +} + +/* Leave room for the complete sensor name and resolution on two lines. */ +html:lang(zh) .recent-imagery-readout .ri-strip { + height: 131px; +} +html:lang(zh) .recent-imagery-readout .ri-card { + height: 123px; + grid-template-rows: 42px 13px 22px 11px 18px; +} +html:lang(zh) .recent-imagery-readout .ri-card-sensor { + white-space: normal; + text-overflow: clip; +} From 82c722e6afa4605c2bb1d70f1a3c490b3f6b7ae8 Mon Sep 17 00:00:00 2001 From: Said Turksever Date: Wed, 21 Oct 2026 17:40:49 +0100 Subject: [PATCH 21/25] chore(format): keep format-scope in step with main Co-Authored-By: Claude Opus 5.5 --- scripts/format-scope.json | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/format-scope.json b/scripts/format-scope.json index fb291362d..fd710642d 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -1078,6 +1078,5 @@ "src/ui/statusBadge.test.mjs", "src/ui/cctvBadges.test.mjs", "scripts/qa-status-badges.mjs", - "src/data/routeSegment.test.mjs", "src/dockerTrustedPeers.test.mjs" ] From f6517691106751238c7eb2250a666a973d524188 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Thu, 22 Oct 2026 12:12:39 +0400 Subject: [PATCH 22/25] i18n(zh-CN): apply the catalog review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of all 2,677 zh-CN entries (scripted checks + full meaning review) confirmed the catalog fully Simplified and mechanically clean. 83 fixes in two passes: Meaning and terms — PARTLY CLOUDY lost 'partly' (多云 → 晴间多云); AIR/SEA WINDOW said scheduled flights (航班 → 空/海监视窗口); Mapped ALPR Cameras collided with ALPR Cameras; globe terms unified on 地球 (球体地图/球体影像); later/earlier observation buttons read as future actions (稍后/早前 → 较晚/较早); 从未叠加… misread as 'never'; 风流线 → 风场流线; 访问飞行器 → 来访航天器; 目击 → 目击者; downrange is 落点距离 not 航程. Polish — 加载 over 载入; 预警 for advisories; {amount}K/M/B 美元; 空载; 窗口 over 视窗; 分享的 {subject} matches its siblings; missing CJK↔Latin spaces restored around {store} and {mode}; 拦截点击, 已被限流, 精选, 逼真世界, and other one-word mainland-usage fixes. --- src/i18n/locales/zh-CN.json | 166 ++++++++++++++++++------------------ 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 6b815f545..ad75de5d4 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -5,7 +5,7 @@ "Click {count} more points.": { "other": "再点击 {count} 个点。" }, - "Those points are in a line — move one off it to enclose an area.": "这些点都在同一条直线上——请移开其中一个点才能围出面。", + "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}", @@ -119,7 +119,7 @@ "EVENT RECONSTRUCTION · 26 AUG 2026": "事件重构 · 2026-08-26", "BHOTE KOSHI OUTBURST FLOOD": "BHOTE KOSHI 溃决洪水", "Close event layer": "关闭事件图层", - "OBSERVED IMAGERY": "实测影像", + "OBSERVED IMAGERY": "观测影像", "SCHEMATIC CORRIDOR": "示意走廊", "2021 HISTORICAL REFERENCE / 2026 POST-EVENT": "2021 年历史参考 / 2026 年灾后", "Historical reference and post-event image split": "历史参考与灾后影像的分割", @@ -131,7 +131,7 @@ "CAPTURE TIME UNVERIFIED": "拍摄时间未核实", "Next story beat": "下一个事件节点", "▶ PLAY": "▶ 播放", - "Play the next shot and continue through this scene": "播放下一个镜头并继续整个场景", + "Play the next shot and continue through this scene": "播放下一个镜头,继续播放整个场景", "▶ PLAY SCENE": "▶ 播放场景", "◉ CINEMATIC": "◉ 运镜", "↺ FULL STORY": "↺ 完整故事", @@ -141,7 +141,7 @@ "GEOLOCATIONS · {credit}": "地理定位 · {credit}", "↗ OPEN PUBLIC MAP": "↗ 打开公共地图", " Clouds are preserved from the source imagery.": "云层按源影像原样保留。", - "Rasuwagadhi witness": "Rasuwagadhi 目击", + "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}%", @@ -163,13 +163,13 @@ "other": "{count} 个来源" }, "OUTSIDE IMAGERY SWIPE": "不在影像对比范围内", - "{scene} / AUTHORED SHOT": "{scene} / 排定镜头", + "{scene} / AUTHORED SHOT": "{scene} / 编排镜头", "NEPAL FLOOD INCIDENT": "尼泊尔洪水事件", "Play {shot}": "播放 {shot}", "current scene shot": "当前场景镜头", "Play reconstruction": "播放重构", - "Load previous scene shot": "载入上一个场景镜头", - "Load next scene shot": "载入下一个场景镜头", + "Load previous scene shot": "加载上一个场景镜头", + "Load next scene shot": "加载下一个场景镜头", "SCHEMATIC CORRIDOR, NOT MODELED ARRIVAL TIME.": "示意走廊,并非模拟的到达时间。", "Rasuwa · 26 Aug 2026": "Rasuwa · 2026-08-26", "Event reconstruction": "事件重构", @@ -261,7 +261,7 @@ "Directions": "路线导航", "Recent imagery": "近期影像", "STATION": "空间站", - "Crewed stations and their visiting vehicles": "载人空间站及其访问飞行器", + "Crewed stations and their visiting vehicles": "载人空间站及其来访航天器", "NAV": "导航", "GNSS navigation — GPS, GLONASS, Galileo": "GNSS 导航:GPS、GLONASS、Galileo", "GEO": "GEO", @@ -296,11 +296,11 @@ "ENVIRONMENTAL": "环境", "EARTH WATCH": "地球监测", "ACTIVE EVENTS": "活跃事件", - "Starting live contacts…": "正在启动实时目标…", + "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.": "仿佛闯进了一间机密座舱——随后你会发现,数据来源全部公开,数据全部真实。", + "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.": "此浏览器阻止了存储,因此无法保存。", @@ -337,7 +337,7 @@ "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}", + "{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}", "N": "北", @@ -350,7 +350,7 @@ "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 the machine running the server": "服务商设置仅响应服务器所在的本机", "Provider Settings answers only local hostnames": "服务商设置只响应本地主机名", "Provider Settings requires an exact local Origin": "服务商设置要求完全匹配的本地 Origin", "Unrecognized Origin refused": "已拒绝无法识别的 Origin", @@ -385,7 +385,7 @@ "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)", + "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 ↗": "管理 ↗", @@ -400,20 +400,20 @@ "your local .env": "你的本地 .env", "Saving…": "正在保存…", "Save failed: {detail}": "保存失败:{detail}", - "Paste at least one key first.": "请先至少粘贴一个密钥。", - "Saved to {store}. Restarting — this page reloads itself.": "已保存到{store}。正在重启——本页面将自动重新加载。", + "Paste at least one key first.": "请先粘贴至少一个密钥。", + "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 登录。", + "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}中移除。正在重启——本页面将自动重新加载。", + "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": "地点信息 + 街景备用;可选的独立密钥", "Voice control — API key or ChatGPT OAuth": "语音控制——API 密钥或 ChatGPT OAuth", @@ -421,12 +421,12 @@ "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)": "更多航班轮询额度(匿名也可使用)", + "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": "暂无当前读数", + "No current readings": "暂无最新读数", "{count} stations reporting": { "other": "{count} 个站点上报数据" }, @@ -447,7 +447,7 @@ "Malformed AQHI observation response": "AQHI 观测响应格式错误", "Camera source temporarily unavailable": "摄像头数据源暂时不可用", "Mapped camera locations": "地图标注的摄像头位置", - "SHOW NEAREST": "最近摄像头", + "SHOW NEAREST": "显示最近", "Stop following the current object before navigating to a camera": "请先停止跟踪当前目标,再前往摄像头", "Move to the nearest loaded camera and show its details": "前往最近已加载的摄像头并显示详情", "Camera badges": "摄像头标记", @@ -462,18 +462,18 @@ "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": "正在显示缓存位置", + "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": "摄像头覆盖不可用", + "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": "选择一个航班、船舶或地图标注设施", + "SELECT A FLIGHT, VESSEL, OR MAPPED INSTALLATION": "选择航班、船舶或地图标注设施", "ENABLE TO LOAD OBSERVED / MAPPED PROXIMITY": "启用以加载观测 / 地图标注的邻近信息", "Unavailable": "不可用", "Focus {label}": "聚焦 {label}", @@ -569,7 +569,7 @@ "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 (HTTP {status})": "路线规划不可用(HTTP {status})", "Routing unavailable": "路线规划不可用", "direction|left": "向左", "direction|right": "向右", @@ -667,7 +667,7 @@ "{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", + "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} 个转向", @@ -684,7 +684,7 @@ "Routing unavailable — no response from the routing service": "路线规划不可用——路线服务无响应", "Earthquake source unavailable": "地震数据源不可用", "Focus fire detection {title}, {details}": "聚焦火点探测 {title},{details}", - "live feed unavailable": "实时数据不可用", + "live feed unavailable": "实时数据源不可用", "{value} conf": "置信度 {value}", "{value} ago": "{value}前", "sensor n/a": "传感器 n/a", @@ -765,7 +765,7 @@ "UNSPECIFIED": "未指明", "+{count} additional payload records": "另有 {count} 条有效载荷记录", "PAYLOAD DATA UNAVAILABLE": "有效载荷数据不可用", - "{value} KM DOWNRANGE": "航程 {value} km", + "{value} KM DOWNRANGE": "落点距离 {value} km", "POSITION UNAVAILABLE": "位置不可用", "FLIGHT {count}": "第 {count} 次飞行", "REUSED": "复用", @@ -806,7 +806,7 @@ "SHOW ALL / DESELECT": "显示全部 / 取消选择", "Pause replay": "暂停回放", "Cancel replay": "取消回放", - "EST. DOWNRANGE": "预估航程", + "EST. DOWNRANGE": "预估落点距离", "PLANNED · ": "计划 · ", "SUPPLIED TRAJECTORY POINTS": "提供的轨迹点", "RECONSTRUCTED ESTIMATE": "重建估算", @@ -849,9 +849,9 @@ "Type unknown": "类型未知", "Reg unknown": "注册号未知", "Operator unknown": "运营方未知", - "${amount}K": "{amount} 千美元", - "${amount}M": "{amount} 百万美元", - "${amount}B": "{amount} 十亿美元", + "${amount}K": "{amount}K 美元", + "${amount}M": "{amount}M 美元", + "${amount}B": "{amount}B 美元", "fire cause|Human": "人为", "fire cause|Natural": "自然", "fire cause|Undetermined": "待定", @@ -901,7 +901,7 @@ "Directory refresh failed; showing the previous station catalog.": "目录刷新失败;正在显示上一次的电台目录。", "Radio directory is temporarily unavailable.": "电台目录暂时不可用。", "radio|NEWS": "新闻", - "radio|TALK": "访谈", + "radio|TALK": "脱口秀", "radio|WEATHER": "天气", "radio|SAFETY": "安全", "radio|AIR / SEA": "航空 / 海事", @@ -942,7 +942,7 @@ "{min}–{max}% scene cloud": "整景云量 {min}–{max}%", "Comparison in use by another scene": "对比功能正被另一个场景占用", "Esri map unavailable · no swipe": "Esri 地图不可用 · 无法滑动对比", - "Swipe needs a globe map": "滑动对比需要球体地图", + "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": "目录已截断 · 可能还有更近的少云日期", @@ -990,7 +990,7 @@ "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,且本站点在允许的引荐来源(referrer)中", + "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,且本站点在允许的引荐来源(referrer)中", "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": "街景没有此地的影像", @@ -1030,8 +1030,8 @@ "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 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": "本地路面仍在加载", @@ -1060,14 +1060,14 @@ "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 rate-limited": "OpenFreeMap 瓦片已被限流", "OpenFreeMap tiles timed out": "OpenFreeMap 瓦片超时", "OpenFreeMap tiles unavailable": "OpenFreeMap 瓦片不可用", - "occupancy|empty": "空车", + "occupancy|empty": "空载", "occupancy|many seats available": "空座较多", "occupancy|few seats available": "空座较少", "occupancy|standing room only": "仅剩站位", - "occupancy|crushed standing room only": "拥挤,仅剩站位", + "occupancy|crushed standing room only": "极度拥挤,仅剩站位", "occupancy|full": "已满", "occupancy|not accepting passengers": "不载客", "occupancy|not boardable": "无法上车", @@ -1120,7 +1120,7 @@ "feed down — retrying slowly": "数据源中断——正在低频重试", "reconnecting to feed…": "正在重新连接数据源…", "Focus vessel {name}, MMSI {mmsi}": "聚焦船舶 {name},MMSI {mmsi}", - "Hidden by this map source · choose a globe map": "已被当前地图源隐藏 · 请选择球体地图", + "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": "帧不可用;保留上一帧观测", @@ -1137,7 +1137,7 @@ "Observation unavailable": "观测不可用", "Waiting for observation": "等待观测数据", "{detail} · synced": "{detail} · 已同步", - "{detail} · nearest": "{detail} · 最近", + "{detail} · nearest": "{detail} · 最接近", "RADAR REFLECTIVITY · dBZ": "雷达反射率 · dBZ", "LIGHTNING DENSITY · 15 min accumulation": "闪电密度 · 15 分钟累计", "GLOBAL INFRARED · hourly": "全球红外 · 每小时", @@ -1147,9 +1147,9 @@ "frame {index}/{total}": "第 {index}/{total} 帧", "loading": "加载中", "Observation: unavailable": "观测:不可用", - "Contiguous US · gaps ≠ no rain": "美国本土连续区域 · 数据缺失 ≠ 无降雨", + "Contiguous US · gaps ≠ no rain": "美国本土 · 数据缺失 ≠ 无降雨", "Americas + Pacific · not individual strikes": "美洲与太平洋 · 非单次闪击", - "Color: strikes/km²/min ×10³": "颜色:次闪击/km²/分钟 ×10³", + "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": "减弱动效 · 历史回放不可用", @@ -1164,7 +1164,7 @@ "Stale source": "数据源已过期", "Loading next frame…": "正在加载下一帧…", "Map center outside coverage": "地图中心在覆盖范围之外", - "strikes/km²/min ×10³": "次闪击/km²/分钟 ×10³", + "strikes/km²/min ×10³": "闪击次数/km²/分钟 ×10³", "N. America": "北美", "Global": "全球", "Hourly global mosaic; usually 2–3 hours delayed": "每小时全球镶嵌图;通常延迟 2–3 小时", @@ -1181,7 +1181,7 @@ "View coverage": "查看覆盖范围", "{value} strikes/km²/min ×10³ (15-minute density)": "{value} 次闪击/km²/分钟 ×10³(15 分钟密度)", "{value} dBZ radar reflectivity": "{value} dBZ 雷达反射率", - "STALE · cached source metadata": "已过期 · 数据源元数据为缓存", + "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.": "NOAA/NWS 15 分钟闪电密度,来源为 Vaisala NLDN/GLD360。覆盖自 110°E 经太平洋/美洲至 0°,25°S–80°N。不是实时闪击计数、不是全球覆盖,也不构成安全警示。", "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 级。各帧使用数据源公布的精确观测时间。", @@ -1224,7 +1224,7 @@ "Speed": "风速", "Pressure": "气压", "Temperature": "气温", - "Wind trails with no field shading": "仅显示风流线,不叠加气象场填色", + "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": "地面以上两米的气温", @@ -1237,7 +1237,7 @@ "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;显示高度并非天气高度。视图光照仅为可读性服务。动画展示的是单一固定预报内的流场,不推进时间。气象场填色叠加于地球底图或当前的照片级 3D Tiles。", + "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;显示高度并非天气高度。视图光照仅为提高可读性。动画展示的是单一固定预报内的流场,不推进时间。气象场填色叠加于地球底图或当前的照片级 3D Tiles。", "MODEL": "模式", "FIELD": "要素", "UNITS": "单位", @@ -1264,7 +1264,7 @@ "WIND AT {coordinates}": "{coordinates} 的风", "{model} · valid {time}": "{model} · 有效时间 {time}", "Wind speed field unavailable": "风速场不可用", - "Globe imagery unavailable": "球体影像不可用", + "Globe imagery unavailable": "地球影像不可用", "Globe field image unavailable": "地球气象场图像不可用", "Unknown wind model": "未知风场模式", "Unknown weather overlay": "未知天气叠加层", @@ -1323,7 +1323,7 @@ "Seeking scene clock": "正在定位场景时钟", "Camera unavailable — exit cockpit first": "相机不可用——请先退出座舱", "No shots to run": "没有可运行的镜头", - "Scene transition limit reached — load a shot to reset": "已达到场景切换上限——请载入一个镜头以重置", + "Scene transition limit reached — load a shot to reset": "已达到场景切换上限——请加载一个镜头以重置", "Export failed: {error}": "导出失败:{error}", "Project exported": "项目已导出", "Importing project": "正在导入项目", @@ -1350,7 +1350,7 @@ "Omniscience Pullback": "全知拉远", "Global Incident Context": "全球事件背景", "Nepal-Focused Globe Rotation": "聚焦尼泊尔的地球旋转", - "Bhote Koshi Regional Approach": "Bhote Koshi 区域接近", + "Bhote Koshi Regional Approach": "Bhote Koshi 区域推近", "Bhote Koshi Nearby Cities": "Bhote Koshi 周边城市", "Bhote Koshi Incident Corridor": "Bhote Koshi 事件走廊", "Bhote Koshi Flood Path": "Bhote Koshi 洪水路径", @@ -1358,7 +1358,7 @@ "Bhote Koshi Upper Valley": "Bhote Koshi 上游河谷", "Upper Valley Collapse": "上游河谷崩塌", "Debris-Dammed Lake": "碎屑堰塞湖", - "Nepal-China Border Gate": "尼中边境口岸", + "Nepal-China Border Gate": "中尼边境口岸", "Timure Evidence Cluster": "Timure 证据群", "Syabru Besi Passage": "Syabru Besi 通道", "Bidur / Trishuli Consequence": "Bidur / Trishuli 后果", @@ -1385,14 +1385,14 @@ "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.": "确认导入此文件前,不会载入或更改任何内容。", + "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.": "相机经纬度以度为单位,高度为相对椭球面的高度,以米为单位。草稿通过校验后才会替换已保存的场景。", + "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", @@ -1518,7 +1518,7 @@ "FRAME · LOADING": "画面 · 加载中", "FRAME · UNAVAILABLE": "画面 · 不可用", "CALIBRATED": "已校准", - "CURATED": "已整理", + "CURATED": "精选", "RAW PRIOR": "初始估计", "CCTV ON": "CCTV 开", "CCTV OFF": "CCTV 关", @@ -1549,7 +1549,7 @@ "Clear selected data layers": "清除已选数据图层", "CONDITIONS UNKNOWN": "天气状况未知", "CLEAR": "晴", - "PARTLY CLOUDY": "多云", + "PARTLY CLOUDY": "晴间多云", "OVERCAST": "阴", "FOG": "雾", "DRIZZLE": "毛毛雨", @@ -1573,7 +1573,7 @@ "CLOUD UNKNOWN": "云量未知", "{source} · LOCATION QUERY": "{source} · 位置查询", "REGIONAL NEWS": "地区新闻", - "{label} · {radius} AIR/SEA WINDOW": "{label} · {radius} 航班 / 船舶范围", + "{label} · {radius} AIR/SEA WINDOW": "{label} · {radius} 空/海监视窗口", "{scope} · INSTALLATIONS {coverage}": "{scope} · 设施 {coverage}", "CONTEXT STANDBY": "态势待机", "ENABLE GLOBAL CONTEXT FOR PROXIMITY PINGS": "启用全局态势以获取邻近目标", @@ -1615,7 +1615,7 @@ "Noir": "黑白", "Snow": "雪景", "Current cockpit vision style: {style}. Activate for next style.": "当前座舱视觉样式:{style}。点击可切换到下一个样式。", - "Current style: {style} — click for next": "当前样式:{style}——点击切换下一个", + "Current style: {style} — click for next": "当前样式:{style}——点击切换到下一个", "Collapse Contact panel": "收起目标面板", "Expand Contact panel": "展开目标面板", "Collapse contact panel": "收起目标面板", @@ -1667,7 +1667,7 @@ "{layerId} could not {action} cleanly": "{layerId} 未能正常{action}", "start": "启动", "stop": "停止", - "{mode} could not start because another layer did not stop cleanly": "{mode}无法启动,因为另一个图层未能正常停止", + "{mode} could not start because another layer did not stop cleanly": "{mode} 无法启动,因为另一个图层未能正常停止", "Space Missions": "太空任务", "Context": "态势", "Rendered globe frames per second · toggle with `": "每秒渲染的地球帧数 · 按 ` 键切换", @@ -1720,13 +1720,13 @@ "GNSS Integrity": "GNSS 完好性", "Rain radar": "降雨雷达", "Lightning density": "闪电密度", - "Cyclone advisories": "气旋公告", + "Cyclone advisories": "气旋预警", "ALPR Cameras": "ALPR 摄像头", "Live Vessels": "实时船舶", "Bike Share": "共享单车", "Cameras": "摄像头", "Street Level": "街景", - "Mapped ALPR Cameras": "ALPR 摄像头", + "Mapped ALPR Cameras": "地图标注 ALPR 摄像头", "Data Centers": "数据中心", "Active Fires": "活跃火点", "Military aircraft": "军用飞机", @@ -1840,7 +1840,7 @@ "Expand Cockpit Radio controls": "展开座舱电台控件", "CH {index} / {total}": "频道 {index} / {total}", "NO STATIONS": "无电台", - "NO STATION AVAILABLE": "没有可用电台", + "NO STATION AVAILABLE": "无可用电台", "{name}, station {index} of {total}": "{name},第 {index} 个电台,共 {total} 个", "No station available": "没有可用电台", "OFF AIR": "停播", @@ -1898,9 +1898,9 @@ "Radio is disabling…": "电台正在停用…", "Radio lifecycle is uncertain — use Enable or Disable to reconcile": "电台生命周期状态不确定——请使用“启用”或“停用”进行核对", "Clear reading": "清除读数", - "Earlier observation": "早前观测", + "Earlier observation": "较早观测", "Observed history": "观测历史", - "Later observation": "稍后观测", + "Later observation": "较晚观测", "Latest": "最新", "Newest frame per product": "每个产品的最新一帧", "Replay observed history": "回放观测历史", @@ -1939,10 +1939,10 @@ "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 · 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 显示当前日期", + "← → 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 固定", @@ -2010,7 +2010,7 @@ "No shots yet. Use CAPTURE SHOT to save current look.": "暂无镜头。使用“保存镜头”保存当前视角及设置。", "Double-click to rename": "双击重命名", "Shot name": "镜头名称", - "LOAD": "载入", + "LOAD": "加载", "DEL": "删除", "{duration}s + {hold}s": "{duration} 秒 + {hold} 秒", "Choose files": "选择文件", @@ -2035,7 +2035,7 @@ "Shared {subject} could not be restored — feed unavailable": "无法恢复分享的 {subject}——数据源不可用", "Shared {subject} is unavailable": "分享的 {subject} 不可用", "ACQUIRING": "获取中", - "SHARED {subject}": "分享 {subject}", + "SHARED {subject}": "分享的 {subject}", "subject": "目标", "LOADING LIVE DATA": "正在加载实时数据", "LOAD COMPLETE": "加载完成", @@ -2119,7 +2119,7 @@ "LAST {count} DAYS": { "other": "最近 {count} 天" }, - "{window} · SINCE {date}": "{window} · 自 {date}", + "{window} · SINCE {date}": "{window} · 自 {date} 起", "Turn Street Level off": "关闭街景", "Turn Street Level on": "开启街景", "{name} imagery on": "{name} 影像已开启", @@ -2134,10 +2134,10 @@ "other": "此序列共 {n} 张图像 · Esc 清除" }, "{n} streets in view · click a line for its nearest photo": { - "other": "视野内共 {n} 条街道 · 点击线段查看最近的照片" + "other": "视野内共 {n} 条街道 · 点击线段查看距其最近的照片" }, "{n} sequences in view · click a line for its nearest photo": { - "other": "视野内共 {n} 条序列 · 点击线段查看最近的照片" + "other": "视野内共 {n} 条序列 · 点击线段查看距其最近的照片" }, "{n} sequences in view · click a line for its photos": { "other": "视野内共 {n} 条序列 · 点击线段查看其照片" @@ -2163,7 +2163,7 @@ "{time} · {minutes} min ago": "{time} · {minutes} 分钟前", "Active weather": "已启用的天气图层", "LATEST · newest per product": "最新 · 每个产品的最新一帧", - "Forecast · valid {valid} · issued {issued}": "预报 · 有效时间 {valid} · 发布 {issued}", + "Forecast · valid {valid} · issued {issued}": "预报 · 有效时间 {valid} · 发布时间 {issued}", "{detail} · Does not follow history": "{detail} · 不随观测历史变化", "Forecast · valid {valid}": "预报 · 有效时间 {valid}", "voice|OFF": "关", @@ -2297,7 +2297,7 @@ }, "degraded": "降级", "stale": "过期", - "partial": "部分", + "partial": "部分可用", "fallback": "备用", "unavailable": "不可用", "Not answered: {layers}": "未响应:{layers}", @@ -2331,7 +2331,7 @@ "{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}", + "Unknown target layer: {target}": "未知的目标图层:{target}", "nominal": "正常", "{label} on": "{label} 已开启", "Frame aircraft": "将飞机纳入视野", @@ -2362,7 +2362,7 @@ "Layer": "图层", "there": "那里", "Camera range {range}": "相机距离 {range}", - "Within {radius} of the view": "距当前视野 {radius} 内", + "Within {radius} of the view": "当前视野 {radius} 范围内", "Labels on": "标签已开启", "Framing uses the nearest 80 loaded contacts": "调整视野时仅包含已加载的最近 80 个目标", "{distance} from {location}": "距 {location} {distance}", @@ -2477,8 +2477,8 @@ "FROM": "出发", "TO": "到达", "Cockpit briefing controls": "座舱简报控件", - "Previous briefing page": "上一页简报", - "Next briefing page": "下一页简报", + "Previous briefing page": "简报上一页", + "Next briefing page": "简报下一页", "Live signals": "实时信号", "Latest regional news": "最新地区新闻", "Location-based information": "基于位置的信息", @@ -2522,7 +2522,7 @@ "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.": "应用高对比度的黑白电影风格调色。", - "Add a cold, snowy whiteout treatment to the scene.": "为场景添加寒冷的雪天白茫茫效果。", + "Add a cold, snowy whiteout treatment to the scene.": "为场景添加寒冷的暴雪白化效果。", "MAP SOURCE": "地图源", "Map source": "地图源", "Style": "样式", @@ -2536,7 +2536,7 @@ "WEATHER": "天气", "Active weather products": "已启用的天气产品", "RECENT IMAGERY": "近期影像", - "Imagery days in the box": "框选内的影像日期", + "Imagery days in the box": "视野内的影像天数", "CONTEXT": "态势", "Compact Radio controls": "精简电台控件", "Open detailed Radio controls": "打开详细电台控件", @@ -2582,7 +2582,7 @@ "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 图层共享。", - "Use your location for faster ADS-B position decoding": "使用你的位置以更快解码 ADS-B 位置", + "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 接收模式", @@ -2672,7 +2672,7 @@ "Sharpen intensity": "锐化强度", "EXIT CLEAN VIEW": "退出纯净界面", "Return UI controls": "恢复界面控件", - "Initializing photorealistic world...": "正在初始化照片级真实世界…", + "Initializing photorealistic world...": "正在初始化照片级逼真世界…", "DATA LAYERS": "数据图层", "ACQUIRING FRAME": "正在获取画面", "NEAREST": "最近", @@ -2697,7 +2697,7 @@ "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 image and deselect it on the map": "关闭图像并取消其在地图上的选中状态", "Close the street-level image": "关闭街景图像", "Street-level viewer": "街景查看器", "Open this image on the provider's site": "在服务商网站上打开此图像", @@ -2708,7 +2708,7 @@ "Panorama filter": "全景筛选", "ALL": "全部", "FLAT": "平面", - "SINCE": "时间", + "SINCE": "起始时间", "SCENES": "场景", "Scene recipe": "场景方案", "NEW": "新建", @@ -2741,7 +2741,7 @@ "Aircraft, vessels and nearby intelligence": "飞机、船舶及周边情报", "Launches, spacecraft and orbital context": "发射、航天器与轨道态势", "EXPLORE MANUALLY": "手动探索", - "Begin with a clean globe": "从未叠加数据图层的地球开始", + "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.": "提示:可通过停靠栏中的 GEV 麦克风按钮用语音操控地图。", From 4bf852298575c0f5d4731687a282c357111c2005 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Thu, 22 Oct 2026 13:50:19 +0400 Subject: [PATCH 23/25] i18n(zh-CN): apply the second-pass review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fresh full review of the catalog (52 changes in 48 entries): - Age readouts that render as bare durations got their missing 前 (LIVE · updated {age}, POSITION {age}). - Unified the rate-limit family on 已被限流 (12 entries had 已限流, 正在限制请求频率 or 受到频率限制). - Unified photorealistic on 照片级逼真 (planet strings, 3D Tiles). - Round-1 stragglers: 括标 → 括号标记, FLIGHT/VESSEL WINDOW → 监视窗口, mapped-ALPR loading strings, plain Talk → 脱口秀, the hardcoded Current style twin. - Collocations: 与…同源, 不超过…上限, 单击放置顶点, 随语音分享. - Mainland usage and consistency polish: 气象高度, 地面风, 标签页, 已获取航迹 (acquire ≠ lock), 缩放级别 6, 发布时间, 独立视角, 视野中心, 播报当前视野, and similar one-word fixes. Kept deliberately: the 图像 (image) vs 影像 (imagery) distinction, which mirrors the English and is consistent across the catalog. --- src/i18n/locales/zh-CN.json | 98 ++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index ad75de5d4..aee065901 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -219,7 +219,7 @@ "That file has an unreadable image or exceeds the 16-megapixel image limit.": "该文件包含无法读取的图像,或图像超过 1600 万像素上限。", "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.": "该文件无法获取地面高度。请在地图加载完成后重试。", + "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。", @@ -230,7 +230,7 @@ "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 rate-limited": "Overpass 已被限流", "Overpass timed out": "Overpass 超时", "Overpass could not complete the query": "Overpass 无法完成查询", "Map tiles temporarily unavailable": "地图瓦片暂时不可用", @@ -269,7 +269,7 @@ "VISUAL": "目视可见", "Brightest naked-eye objects — CelesTrak visual group": "最亮的肉眼可见天体:CelesTrak visual 组", "COMMS": "通信", - "Broadband constellation shell — shown only in DENSE mode": "宽带星座:仅在“密集”模式下显示", + "Broadband constellation shell — shown only in DENSE mode": "宽带星座壳层——仅在“密集”模式下显示", "FISHING": "渔船", "TOWING": "拖带", "DREDGER": "挖泥船", @@ -350,7 +350,7 @@ "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 the machine running the server": "服务商设置仅响应运行服务器的那台机器", "Provider Settings answers only local hostnames": "服务商设置只响应本地主机名", "Provider Settings requires an exact local Origin": "服务商设置要求完全匹配的本地 Origin", "Unrecognized Origin refused": "已拒绝无法识别的 Origin", @@ -414,7 +414,7 @@ "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 地球、地点搜索 + 街景", + "The photorealistic 3D planet, place search + Street View": "照片级逼真 3D 地球、地点搜索 + 街景", "Places context + Street View fallback; optional separate key": "地点信息 + 街景备用;可选的独立密钥", "Voice control — API key or ChatGPT OAuth": "语音控制——API 密钥或 ChatGPT OAuth", "Live ships, worldwide": "全球实时船舶", @@ -458,8 +458,8 @@ "{count} nearby": { "other": "附近 {count} 个" }, - "retrying mapped ALPR cameras": "正在重试加载 ALPR 摄像头", - "loading mapped ALPR cameras": "正在加载 ALPR 摄像头", + "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": "当前显示的是缓存位置", @@ -484,7 +484,7 @@ "Next — nearest unvisited contact in the 250 km window": "下一个:250 km 范围内最近的未访问目标", "NEXT": "下一个", "Named areas ({n})": "已命名区域({n})", - "{distance} FLIGHT / VESSEL WINDOW": "{distance} 航班 / 船舶范围", + "{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 普查", @@ -518,7 +518,7 @@ "SPEC FOV {span}° (CAPABILITY)": "规格 FOV {span}°(可调范围)", "COVERAGE {value}km²": "覆盖 {value}km²", "OVERLAP {count} cams": "重叠 {count} 个摄像头", - "ISOLATED VIEW": "孤立视角", + "ISOLATED VIEW": "独立视角", "PROJ MONITOR": "投影:监视器", "PROJ OFF": "投影:关", "VIEWSHED": "可视域", @@ -556,15 +556,15 @@ "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 公告。该区域表示气旋中心路径预报的不确定范围,不代表气旋大小或全部灾害影响范围。预报点标注的是数据源的预报时效(小时),不是按公告发布时刻推算的时间。几何图形贴地显示;显示高度并非天气高度。请以官方公告为准。", + "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": "路线服务已限流", + "rate limited": "已被限流", + "routing service is rate limited": "路线服务已被限流", "route leg too long": "路段过长", "route too long": "路线过长", "{error} — try again in a moment": "{error}——请稍后重试", @@ -599,8 +599,8 @@ "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, on the left": "到达目的地,位于左侧", + "Arrive at your destination, on the right": "到达目的地,位于右侧", "Arrive at your destination": "到达目的地", "Make a U-turn onto {road}": "掉头进入 {road}", "Make a U-turn": "掉头", @@ -694,7 +694,7 @@ "FIRE": "火点", "FIRES": "火点", "max {value} MW": "最大 {value} MW", - "new {value}": "最新 {value}前", + "new {value}": "最新({value}前)", "{value}°N": "{value}°N", "{value}°S": "{value}°S", "{value}°E": "{value}°E", @@ -714,7 +714,7 @@ "refreshing...": "刷新中…", "loading...": "加载中…", "KEY REQUIRED": "需要密钥", - "LIVE · updated {age}": "实时 · 更新于 {age}", + "LIVE · updated {age}": "实时 · 更新于 {age}前", "Source snapshot time unavailable": "来源快照时间不可用", "Live data unavailable": "实时数据不可用", "Low-accuracy score < 2%": "低精度评分 < 2%", @@ -724,10 +724,10 @@ "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.": "平移或缩放后以及每分钟自动更新。公共数据源覆盖以视图中心为圆心、半径 250 海里的范围;缩小地图并不会使其成为全球快照。空白网格位于采样覆盖范围之外,或观测数据不足。", + "Updates automatically after pan or zoom and once a minute. The public feed covers a 250-nautical-mile circle around the view centre; zooming out does not make it a worldwide snapshot. Blank cells are outside sampled coverage or have insufficient observations.": "平移或缩放后以及每分钟自动更新。公共数据源覆盖以视野中心为圆心、半径 250 海里的范围;缩小地图并不会使其成为全球快照。空白网格位于采样覆盖范围之外,或观测数据不足。", "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:扣除一架飞机,因此并非原始占比。每个网格需在 {minutes} 分钟内有 {aircraft} 架飞机。导航完好性异常不等于检测到干扰或欺骗。", "GNSS snapshot needs a view anchor": "GNSS 快照需要视图锚点", - "GNSS feed rate limited (HTTP 429)": "GNSS 数据源已限流(HTTP 429)", + "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": "机场", @@ -787,7 +787,7 @@ "LAUNCH TIME · ": "发射时间 · ", "ORBIT · ": "轨道 · ", "ASCENT PATH · ": "上升轨迹 · ", - "CURRENT DISTANCE FROM EARTH · ": "当前距地球距离 · ", + "CURRENT DISTANCE FROM EARTH · ": "当前与地球的距离 · ", "SATELLITE SPEED · ": "卫星速度 · ", "PAYLOAD": "有效载荷", "NAME": "名称", @@ -831,7 +831,7 @@ "ALT {altitude} FT · GS {speed} KT · TRK {track}": "高度 {altitude} ft · 地速 {speed} kn · 航迹角 {track}", "V/S {rate}": "升降率 {rate}", "V/S {rate} FPM": "升降率 {rate} ft/min", - "POSITION {age}": "位置 {age}", + "POSITION {age}": "位置 {age}前", "POSITION {seconds} S AGO": "位置 {seconds} 秒前", "{count} MSGS": { "other": "{count} 条消息" @@ -889,7 +889,7 @@ "Malformed perimeter snapshot": "周界快照格式错误", "radio tag|All": "全部", "News": "新闻", - "Talk": "访谈", + "Talk": "脱口秀", "Weather / Emergency": "天气 / 应急", "Public Safety": "公共安全", "Aviation / Marine": "航空 / 海事", @@ -998,19 +998,19 @@ "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 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 is rate-limiting requests": "KartaView 已被限流", "KartaView sent too much data": "KartaView 返回的数据过多", - "KartaView is rate-limiting requests — try again in a moment": "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": "覆盖范围请求受到频率限制——稍后会自动恢复", + "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 照片", @@ -1184,7 +1184,7 @@ "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.": "NOAA/NWS 15 分钟闪电密度,来源为 Vaisala NLDN/GLD360。覆盖自 110°E 经太平洋/美洲至 0°,25°S–80°N。不是实时闪击计数、不是全球覆盖,也不构成安全警示。", - "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 级。各帧使用数据源公布的精确观测时间。", + "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": "图像", @@ -1229,15 +1229,15 @@ "Sea-level air pressure: broad highs and lows": "海平面气压:大范围的高压与低压", "Air temperature two meters above the surface": "地面以上两米的气温", "Wind speed units": "风速单位", - "Read wind at map center": "读取地图中心气象数据", + "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}", + "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;显示高度并非天气高度。视图光照仅为提高可读性。动画展示的是单一固定预报内的流场,不推进时间。气象场填色叠加于地球底图或当前的照片级 3D Tiles。", + "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;显示高度并非气象高度。视图光照仅为提高可读性。动画展示的是单一固定预报内的流场,不推进时间。气象场填色叠加于地球底图或当前的照片级逼真 3D Tiles。", "MODEL": "模式", "FIELD": "要素", "UNITS": "单位", @@ -1509,7 +1509,7 @@ "Fallback Street View frame": "备用街景画面", "Upstream unavailable": "上游不可用", "No source configured": "未配置来源", - "Configured source": "已配置数据源", + "Configured source": "已配置来源", "Configured Source": "已配置数据源", "Synthetic fallback": "合成备用", "Upstream HTTP {status}": "上游 HTTP {status}", @@ -1560,7 +1560,7 @@ "MIXED CONDITIONS": "混合天气", "CYCLE ON": "轮换开", "CYCLE OFF": "轮换关", - "Stop automatic page cycling. Previous, Next, and the SIG/NEWS/LOCAL tabs stay available.": "停止自动翻页。上一页、下一页以及“信号”“新闻”“本地”标签仍可使用。", + "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": "地区新闻不可用", @@ -1576,7 +1576,7 @@ "{label} · {radius} AIR/SEA WINDOW": "{label} · {radius} 空/海监视窗口", "{scope} · INSTALLATIONS {coverage}": "{scope} · 设施 {coverage}", "CONTEXT STANDBY": "态势待机", - "ENABLE GLOBAL CONTEXT FOR PROXIMITY PINGS": "启用全局态势以获取邻近目标", + "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": "目标已脱离其数据源 · 读数保持最后已知值", @@ -1647,7 +1647,7 @@ "SOURCE STATUS UNAVAILABLE": "数据源状态不可用", "Restore real aircraft": "恢复真实飞机", "Reclassify as TR-3B": "重新归类为 TR-3B", - "TRACK ACQUIRED": "已锁定航迹", + "TRACK ACQUIRED": "已获取航迹", "{label} · COURSE {heading}°": "{label} · 航向 {heading}°", "Context could not restore every layer; try again": "态势未能恢复所有图层,请重试", "No selected data layers": "没有已选数据图层", @@ -1938,13 +1938,13 @@ "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": "拖动分割线 · “交换”互换两侧", + "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 限制", + "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": "模式", @@ -1985,7 +1985,7 @@ "Acquired {time}": "获取于 {time}", "Coverage {coverage}": "覆盖 {coverage}", "START HERE · {reason}": "从此开始 · {reason}", - "Daily overview shows little detail in a box this small": "框选这么小时,每日概览几乎看不出细节", + "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": "时间、覆盖与数据源", @@ -2048,7 +2048,7 @@ "RETRYING ALPR CAMERAS": "正在重试 ALPR 摄像头", "FETCHING ALPR CAMERAS": "正在获取 ALPR 摄像头", "OVERPASS TEMPORARILY UNAVAILABLE": "Overpass 暂时不可用", - "OVERPASS RATE-LIMITED": "Overpass 已限流", + "OVERPASS RATE-LIMITED": "Overpass 已被限流", "OVERPASS TIMED OUT": "Overpass 超时", "OVERPASS COULD NOT COMPLETE THE QUERY": "Overpass 无法完成查询", "MAP TILES TEMPORARILY UNAVAILABLE": "地图瓦片暂时不可用", @@ -2076,8 +2076,8 @@ }, "{feed} feed unavailable": "{feed} 数据源不可用", "Source snapshot {minutes} min old": "数据源快照:{minutes} 分钟前", - "{source} rate limited (anonymous)": "{source} 已限流(匿名)", - "{source} rate limited": "{source} 已限流", + "{source} rate limited (anonymous)": "{source} 已被限流(匿名)", + "{source} rate limited": "{source} 已被限流", "{source} network error": "{source} 网络错误", "Malformed {source} aircraft rows": "{source} 飞机数据行格式错误", "Malformed {source} response": "{source} 响应格式错误", @@ -2287,7 +2287,7 @@ "{label} loading": "{label} 加载中", "{label} error": "{label} 出错", "counted the first {returned} loaded records": "已统计最先加载的 {returned} 条记录", - "counted {returned} of {total} loaded records": "已统计已加载 {total} 条记录中的 {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": "目标面板快照——卡片、语音播报和后续追问记忆均来自同一组保留目标", @@ -2415,7 +2415,7 @@ "Layer off": "图层已关闭", "Zoom out": "缩小", "Zoom in": "放大", - "Read the view": "读取视野", + "Read the view": "播报当前视野", "Check view state": "检查视野状态", "Action": "操作", "Finding places": "正在查找地点", @@ -2450,7 +2450,7 @@ "Previous cockpit vision style": "上一个座舱视觉样式", "Previous vision style": "上一个视觉样式", "Current cockpit vision style: NORMAL. Activate for next style.": "当前座舱视觉样式:常规。点击可切换到下一个样式。", - "Current style: NORMAL — click for next": "当前样式:常规——点击切换下一个", + "Current style: NORMAL — click for next": "当前样式:常规——点击切换到下一个", "CURRENT": "当前", "Next cockpit vision style": "下一个座舱视觉样式", "Next vision style": "下一个视觉样式", @@ -2616,7 +2616,7 @@ "Sonar intensity": "声呐强度", "Opacity": "不透明度", "Cyber contact and label opacity": "赛博目标与标签不透明度", - "Minimum opacity for map dots, icons, models, brackets, and labels between sonar passes": "两次声呐扫描之间地图点、图标、模型、括标与标签的最低不透明度", + "Minimum opacity for map dots, icons, models, brackets, and labels between sonar passes": "两次声呐扫描之间地图点、图标、模型、括号标记与标签的最低不透明度", "Sector": "扇区", "Sonar sector width": "声呐扇区宽度", "Detection Overlay (D)": "探测叠加层(D)", @@ -2642,7 +2642,7 @@ "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 取消", + "Draw on the world — click vertices, double-click or Enter to finish, Esc to cancel": "在地球上绘制——单击放置顶点,双击或按 Enter 完成,按 Esc 取消", "Shape": "形状", "Shape to draw": "要绘制的形状", "Area": "面", @@ -2658,7 +2658,7 @@ "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 文件(也可直接拖放到地球上)。文件内容仅保留在本设备上;叠加层可能出现在共享给语音的地图截图中。", + "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": "已导入的文件", @@ -2694,7 +2694,7 @@ "Turn Street Level on or off": "开启或关闭街景", "Image fit": "图像适配", "FIT": "适应", - "Fit: show the whole photo, with dark bars where its shape differs from the frame": "适应:显示整张照片,比例与画框不同处留黑边", + "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": "关闭图像并取消其在地图上的选中状态", @@ -2725,7 +2725,7 @@ "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 密钥提供照片级 3D 地球,其余数据源都叠加在其上。", + "The Google Maps key buys the photorealistic planet — everything else stacks on top.": "Google Maps 密钥提供照片级逼真 3D 地球,其余数据源都叠加在其上。", "NO PLACE LEFT BEHIND": "不遗漏任何地方", "ACTIVE STYLE": "当前样式", "Globe actions": "地球操作", @@ -2756,7 +2756,7 @@ "Switch Street Level on to draw its coverage.": "开启“街景”以绘制其覆盖范围。", "Every provider is switched off: light a chip to draw its coverage.": "所有服务商均已关闭:点亮一个标签即可绘制其覆盖范围。", "No sequences in view match the filter": "视野内没有符合筛选条件的序列", - "No coverage in view": "视野内没有覆盖", + "No coverage in view": "视野内无覆盖范围", "Voice provider": "语音服务商", "Changing provider reconnects voice if it is on.": "如语音已开启,切换服务商会重新连接语音。", "Gemini returned an oversized audio chunk": "Gemini 返回的音频块过大", @@ -2769,7 +2769,7 @@ "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 语音需要来自本应用同源的请求", + "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 语音启动过于频繁。请稍后重试。", From 1416266bcb9575219d8159ff8e0ad4fb74be9a59 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Thu, 22 Oct 2026 22:07:09 +0400 Subject: [PATCH 24/25] fix(i18n): avoid duplicate Chinese FIRMS age suffix --- src/i18n/firms-live-zh.test.mjs | 43 +++++++++++++++++++++++++++++++++ src/i18n/locales/zh-CN.json | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/i18n/firms-live-zh.test.mjs diff --git a/src/i18n/firms-live-zh.test.mjs b/src/i18n/firms-live-zh.test.mjs new file mode 100644 index 000000000..649842e7f --- /dev/null +++ b/src/i18n/firms-live-zh.test.mjs @@ -0,0 +1,43 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { getLocale, setLocale } from './index.js'; +import { createModel } from '../layers/firms/model.js'; +import { createQueries } from '../layers/firms/queries.js'; + +const cases = [ + [0, '不到 1 分钟前', '<1m ago'], + [120000, '2 分钟前', '2m ago'], + [7200000, '2 小时前', '2h ago'], +]; + +for (const [age, zh, en] of cases) { + test(`FIRMS live age ${age} ms has one relative-time suffix`, (context) => { + const now = 1800000000000; + context.mock.method(Date, 'now', () => now); + const previousLocale = getLocale(); + const layerState = { + _loading: false, + _stale: false, + _keyRequired: false, + _error: null, + _lastUpdate: now - age, + _count: 1, + _cellCount: 0, + _fires: [], + }; + const model = createModel({ services: { anchors: {} } }); + const { methods } = createQueries({ + layerState, + components: { model }, + config: {}, + }); + try { + setLocale('en'); + assert.equal(methods.getStats().loadingLabel, `LIVE · updated ${en}`); + setLocale('zh-CN'); + assert.equal(methods.getStats().loadingLabel, `实时 · 更新于 ${zh}`); + } finally { + setLocale(previousLocale); + } + }); +} diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index aee065901..0816935f6 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -714,7 +714,7 @@ "refreshing...": "刷新中…", "loading...": "加载中…", "KEY REQUIRED": "需要密钥", - "LIVE · updated {age}": "实时 · 更新于 {age}前", + "LIVE · updated {age}": "实时 · 更新于 {age}", "Source snapshot time unavailable": "来源快照时间不可用", "Live data unavailable": "实时数据不可用", "Low-accuracy score < 2%": "低精度评分 < 2%", From 2c3b364069d59281f6c5ddeac61f2399d894fad2 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Thu, 22 Oct 2026 22:29:24 +0400 Subject: [PATCH 25/25] docs: simplify Chinese localization notes --- changelog.d/1298-simplified-chinese.md | 8 +++----- docs/CURRENT-STATE.md | 5 ++--- docs/translations/zh-CN.md | 4 +--- src/ui/styles/cjk.css | 9 +++------ 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/changelog.d/1298-simplified-chinese.md b/changelog.d/1298-simplified-chinese.md index e6850e964..d35d08cf1 100644 --- a/changelog.d/1298-simplified-chinese.md +++ b/changelog.d/1298-simplified-chinese.md @@ -1,6 +1,4 @@ -- Add Simplified Chinese (zh-CN) as an interface language, selectable from - the globe picker. The catalog builds on the Chinese translations by - @abiyeenzo (#914) and @EDIT-Aeterna (#979); the rest was translated and the - whole catalog reviewed against the screens. Chinese text gets tight tracking - and readable minimum sizes, and year-first dates read 2021年10月16日 +- Add Simplified Chinese (zh-CN) to the language picker, with localized dates + and readable Chinese labels. Based on translations by @abiyeenzo (#914) + and @EDIT-Aeterna (#979). (German, @gekh, #1298). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index de60d3f34..3e61080ab 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -3429,9 +3429,8 @@ Shipped locales: **en** (source and fallback), **es**, **ru** and **zh-CN**. The value). English has no catalog; every other locale is `src/i18n/locales/.json`, and a missing entry falls back to English. - **Locale choice.** `?lang=` → stored choice (`gev:locale:v1`) → - `navigator.languages` → English. Any `zh` tag maps to `zh-CN` except - Traditional ones (`Hant`, `TW`, `HK`, `MO`), which fall through until a - Traditional catalog ships. `?lang=` is not stored, and share links drop + `navigator.languages` → English. Chinese tags map to `zh-CN`; tags containing + `Hant`, `TW`, `HK` or `MO` are skipped. `?lang=` is not stored, and share links drop it, so a recipient keeps their own language. The globe picker in the bottom-right corner (`src/ui/localePicker.js`) stores the choice, drops `?lang=` (keeps it only when storage is blocked) and reloads. It asks first diff --git a/docs/translations/zh-CN.md b/docs/translations/zh-CN.md index 33f7c7251..e3b7bcad1 100644 --- a/docs/translations/zh-CN.md +++ b/docs/translations/zh-CN.md @@ -2,9 +2,7 @@ Catalog: `src/i18n/locales/zh-CN.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md). -The catalog started from the Chinese locale by abiyeenzo (#914) and the -Simplified Chinese localization by EDIT-Aeterna (#979); the rest was translated -for the current English and reviewed against both. +Translation sources: abiyeenzo (#914) and EDIT-Aeterna (#979). ## Style diff --git a/src/ui/styles/cjk.css b/src/ui/styles/cjk.css index 9ecfa0ce3..3c01d3987 100644 --- a/src/ui/styles/cjk.css +++ b/src/ui/styles/cjk.css @@ -1,7 +1,5 @@ -/* Chinese text. The UI sets Latin capitals with wide tracking; Chinese set - that way falls apart into single glyphs, so its tracking stays tight. The - intel HUD readouts (codes, coordinates) and text kept in English on purpose - (translate="no", and whatever holds it) keep the Latin look. */ +/* Tight tracking keeps Chinese labels readable; preserve Latin tracking for + intel HUD readouts and content marked translate="no", including its containers. */ html:lang(zh) body :not( @@ -14,8 +12,7 @@ html:lang(zh) letter-spacing: 0.02em !important; } -/* Below about 9px a Chinese glyph blurs: lift the dock's voice labels and the - panel layer buttons to that floor. */ +/* Keep small Chinese labels legible at a minimum of 9px. */ html:lang(zh) #command-dock .gev-mic-label, html:lang(zh) #command-dock #gev-voice-detail, html:lang(zh) .gev-voice-tier-btn,