1642 lines
74 KiB
JavaScript
1642 lines
74 KiB
JavaScript
const DEFAULT_LOCALE = "zh-CN";
|
||
const LOCALE_STORAGE_KEY = "planet-locale";
|
||
const LEGACY_DOCS_LANG_STORAGE_KEY = "docs-lang";
|
||
const LOCALE_CHANGE_EVENT = "earth:locale-change";
|
||
|
||
const LOCALE_LABELS = {
|
||
"zh-CN": "中文",
|
||
"en-US": "English",
|
||
};
|
||
|
||
const EARTH_MESSAGE_MARKER = "__planetEarthMessage";
|
||
|
||
function localizedParam(value, locale) {
|
||
if (isEarthMessage(value)) return formatEarthMessage(value, locale);
|
||
return translateText(String(value ?? ""), locale);
|
||
}
|
||
|
||
const EARTH_MESSAGE_TEMPLATES = {
|
||
"loading.default": {
|
||
zh: "正在加载...",
|
||
en: "Loading...",
|
||
},
|
||
"loading.initializing": {
|
||
zh: "正在初始化...",
|
||
en: "Initializing...",
|
||
},
|
||
"loading.cableData": {
|
||
zh: "正在加载线缆数据...",
|
||
en: "Loading cable data...",
|
||
},
|
||
"loading.vesselData": {
|
||
zh: "正在加载船只数据...",
|
||
en: "Loading vessel data...",
|
||
},
|
||
"loading.satelliteData": {
|
||
zh: "正在加载卫星数据...",
|
||
en: "Loading satellite data...",
|
||
},
|
||
"loading.realTerrainData": {
|
||
zh: "正在加载真实地形数据...",
|
||
en: "Loading real terrain data...",
|
||
},
|
||
"startup.landingPoints": {
|
||
zh: "正在加载登陆点...",
|
||
en: "Loading landing points...",
|
||
},
|
||
"startup.cables": {
|
||
zh: "正在加载海缆...",
|
||
en: "Loading subsea cables...",
|
||
},
|
||
"startup.satellites": {
|
||
zh: "正在加载卫星...",
|
||
en: "Loading satellites...",
|
||
},
|
||
"startup.vessels": {
|
||
zh: "正在加载船只...",
|
||
en: "Loading vessels...",
|
||
},
|
||
"startup.bgp": {
|
||
zh: "正在加载BGP态势...",
|
||
en: "Loading BGP situation...",
|
||
},
|
||
"startup.clouds": {
|
||
zh: "正在加载大气云图...",
|
||
en: "Loading atmosphere and clouds...",
|
||
},
|
||
"startup.landOceanBase": {
|
||
zh: "正在加载海陆基座...",
|
||
en: "Loading land/ocean base...",
|
||
},
|
||
"startup.computeCenters": {
|
||
zh: "正在加载算力中心...",
|
||
en: "Loading compute centers...",
|
||
},
|
||
"startup.hdTexture": {
|
||
zh: "正在启用高清材质...",
|
||
en: "Enabling HD texture...",
|
||
},
|
||
"startup.terrain": {
|
||
zh: "正在渲染地形...",
|
||
en: "Rendering terrain...",
|
||
},
|
||
"status.dataLoaded": {
|
||
zh: "数据已加载",
|
||
en: "Data loaded",
|
||
},
|
||
"status.layoutExpanded": {
|
||
zh: ({ expanded }) => (expanded ? "布局已最大化" : "布局已恢复"),
|
||
en: ({ expanded }) => (expanded ? "Layout maximized" : "Layout restored"),
|
||
},
|
||
"status.layerUnavailable": {
|
||
zh: ({ layer }) => `${localizedParam(layer, "zh-CN")}当前不可用`,
|
||
en: ({ layer }) => `${localizedParam(layer, "en-US")} unavailable`,
|
||
},
|
||
"status.layerVisibility": {
|
||
zh: ({ layer, visible }) => `${localizedParam(layer, "zh-CN")}${visible ? "已显示" : "已隐藏"}`,
|
||
en: ({ layer, visible }) => `${localizedParam(layer, "en-US")} ${visible ? "shown" : "hidden"}`,
|
||
},
|
||
"status.layerEnabled": {
|
||
zh: ({ layer, enabled }) => `${localizedParam(layer, "zh-CN")}${enabled ? "已启用" : "已隐藏"}`,
|
||
en: ({ layer, enabled }) => `${localizedParam(layer, "en-US")} ${enabled ? "enabled" : "hidden"}`,
|
||
},
|
||
"status.loadedCables": {
|
||
zh: ({ count }) => `成功加载 ${count} 条电缆`,
|
||
en: ({ count }) => `Loaded ${count} cables`,
|
||
},
|
||
"status.loadedLandingPoints": {
|
||
zh: ({ count }) => `成功加载 ${count} 个登陆点`,
|
||
en: ({ count }) => `Loaded ${count} landing points`,
|
||
},
|
||
"status.locked": {
|
||
zh: ({ name }) => `已锁定: ${name}`,
|
||
en: ({ name }) => `Locked: ${name}`,
|
||
},
|
||
"status.located": {
|
||
zh: ({ target, name }) => `已定位${localizedParam(target, "zh-CN")}:${name}`,
|
||
en: ({ target, name }) => `Located ${localizedParam(target, "en-US").toLowerCase()}: ${name}`,
|
||
},
|
||
"status.selected": {
|
||
zh: ({ target, name }) => (target ? `已选择${localizedParam(target, "zh-CN")}: ${name}` : `已选择: ${name}`),
|
||
en: ({ target, name }) => (target ? `Selected ${localizedParam(target, "en-US")}: ${name}` : `Selected: ${name}`),
|
||
},
|
||
"status.bgpSelected": {
|
||
zh: ({ collector, regionCount, cableCount }) =>
|
||
`已选择BGP事件: ${collector} · ${regionCount}个区域 / ${cableCount}条相关海缆`,
|
||
en: ({ collector, regionCount, cableCount }) =>
|
||
`Selected BGP event: ${collector} · ${regionCount} regions / ${cableCount} related cables`,
|
||
},
|
||
"status.computeCoordinatesSaved": {
|
||
zh: "算力中心坐标已保存",
|
||
en: "Compute center coordinates saved",
|
||
},
|
||
"status.coordinatesSavedLater": {
|
||
zh: "坐标已保存,地图稍后同步",
|
||
en: "Coordinates saved; map will sync shortly",
|
||
},
|
||
"status.coordinatesSavedSyncing": {
|
||
zh: "坐标已保存,正在同步地图...",
|
||
en: "Coordinates saved; syncing map...",
|
||
},
|
||
"status.newsPanel": {
|
||
zh: ({ open }) => (open ? "Live 新闻窗口已打开" : "Live 新闻窗口已关闭"),
|
||
en: ({ open }) => (open ? "Live News panel opened" : "Live News panel closed"),
|
||
},
|
||
"status.newsRefresh": {
|
||
zh: ({ ok }) => (ok ? "态势新闻已刷新" : "态势新闻刷新失败"),
|
||
en: ({ ok }) => (ok ? "Situation news refreshed" : "Situation news refresh failed"),
|
||
},
|
||
"status.newsSyncFailed": {
|
||
zh: "态势新闻同步失败",
|
||
en: "Situation news sync failed",
|
||
},
|
||
"status.languageSwitched": {
|
||
zh: "语言已切换为中文",
|
||
en: "Language switched to English",
|
||
},
|
||
"status.copyEmpty": {
|
||
zh: "无可复制内容",
|
||
en: "Nothing to copy",
|
||
},
|
||
"status.copyValue": {
|
||
zh: ({ label, value }) => `已复制${label}:${value}`,
|
||
en: ({ label, value }) => `Copied ${label}: ${value}`,
|
||
},
|
||
"status.copyFailed": {
|
||
zh: "复制失败",
|
||
en: "Copy failed",
|
||
},
|
||
"status.zoomPercent": {
|
||
zh: ({ percent }) => `缩放 ${percent}%`,
|
||
en: ({ percent }) => `Zoom ${percent}%`,
|
||
},
|
||
"status.zoomReset": {
|
||
zh: ({ zoom }) => `缩放已重置到${zoom}`,
|
||
en: ({ zoom }) => `Zoom reset to ${zoom}`,
|
||
},
|
||
"status.runtimePaused": {
|
||
zh: ({ label, active }) => `${localizedParam(label, "zh-CN")}${active ? "已恢复" : "已暂停"}`,
|
||
en: ({ label, active }) => `${localizedParam(label, "en-US")} ${active ? "resumed" : "paused"}`,
|
||
},
|
||
"status.valueChanged": {
|
||
zh: ({ label, value }) => `${localizedParam(label, "zh-CN")}已切换为:${localizedParam(value, "zh-CN")}`,
|
||
en: ({ label, value }) => `${localizedParam(label, "en-US")} switched to ${localizedParam(value, "en-US")}`,
|
||
},
|
||
"status.modulesChanged": {
|
||
zh: ({ label, value }) => `${localizedParam(label, "zh-CN")}已切换为:${value}`,
|
||
en: ({ label, value }) => `${localizedParam(label, "en-US")} changed to ${value}`,
|
||
},
|
||
"status.updated": {
|
||
zh: ({ label }) => `${localizedParam(label, "zh-CN")}已更新`,
|
||
en: ({ label }) => `${localizedParam(label, "en-US")} updated`,
|
||
},
|
||
"status.booleanSetting": {
|
||
zh: ({ label, enabled }) => `${localizedParam(label, "zh-CN")}${enabled ? "已开启" : "已关闭"}`,
|
||
en: ({ label, enabled }) => `${localizedParam(label, "en-US")} ${enabled ? "enabled" : "disabled"}`,
|
||
},
|
||
"status.motionDebugMode": {
|
||
zh: ({ enabled, pending }) =>
|
||
enabled
|
||
? (pending ? "动捕调试模式将在下次进入动捕时开启" : "动捕调试模式已开启")
|
||
: "动捕调试模式已关闭",
|
||
en: ({ enabled, pending }) =>
|
||
enabled
|
||
? (pending ? "Motion debug mode will enable next time Motion Mode starts" : "Motion debug mode enabled")
|
||
: "Motion debug mode disabled",
|
||
},
|
||
"status.motionProvider": {
|
||
zh: ({ provider }) => (provider === "motion_agent" ? "Motion Agent 已接管动捕输入" : "动捕已切换到浏览器传感器"),
|
||
en: ({ provider }) => (provider === "motion_agent" ? "Motion Agent now controls motion input" : "Motion switched to browser sensors"),
|
||
},
|
||
"status.motionDebugView": {
|
||
zh: ({ skeletonOnly }) => (skeletonOnly ? "动捕调试已切换为只显示骨骼" : "动捕调试已显示实时画面"),
|
||
en: ({ skeletonOnly }) => (skeletonOnly ? "Motion debug switched to skeleton only" : "Motion debug showing live video"),
|
||
},
|
||
"status.motionGesturesUpdated": {
|
||
zh: "动捕识别动作已更新",
|
||
en: "Motion gestures updated",
|
||
},
|
||
"status.settingsReset": {
|
||
zh: "Earth 设置已重置",
|
||
en: "Earth settings reset",
|
||
},
|
||
"status.terrainUnavailable": {
|
||
zh: "真实地形暂时不可用",
|
||
en: "Real terrain is temporarily unavailable",
|
||
},
|
||
"status.boundaryDownloaded": {
|
||
zh: "高精国界已下载并应用",
|
||
en: "High-precision boundaries downloaded and applied",
|
||
},
|
||
"status.boundaryBuildStarted": {
|
||
zh: "高精国界构建已启动",
|
||
en: "High-precision boundary build started",
|
||
},
|
||
"status.boundaryPrecision": {
|
||
zh: ({ high }) => (high ? "已切换到高精国界" : "已切换到低精国界"),
|
||
en: ({ high }) => (high ? "Switched to high-precision boundaries" : "Switched to low-precision boundaries"),
|
||
},
|
||
"status.failure": {
|
||
zh: ({ label, error }) => `${localizedParam(label, "zh-CN")}:${error}`,
|
||
en: ({ label, error }) => `${localizedParam(label, "en-US")}: ${error}`,
|
||
},
|
||
"status.layerLoadFailed": {
|
||
zh: ({ layer, error }) => `${localizedParam(layer, "zh-CN")}加载失败: ${error}`,
|
||
en: ({ layer, error }) => `Failed to load ${localizedParam(layer, "en-US")}: ${error}`,
|
||
},
|
||
"status.loadFailedList": {
|
||
zh: ({ items }) => (Array.isArray(items) ? items : [])
|
||
.map(({ label, reason }) =>
|
||
`${localizedParam(label, "zh-CN")}加载失败: ${reason?.message || String(reason)}`,
|
||
)
|
||
.join(";"),
|
||
en: ({ items }) => (Array.isArray(items) ? items : [])
|
||
.map(({ label, reason }) =>
|
||
`Failed to load ${localizedParam(label, "en-US")}: ${reason?.message || String(reason)}`,
|
||
)
|
||
.join("; "),
|
||
},
|
||
"status.shortcutConflict": {
|
||
zh: ({ owner }) => `快捷键已被「${owner}」使用`,
|
||
en: ({ owner }) => `Shortcut already used by "${owner}"`,
|
||
},
|
||
"status.shortcutsReset": {
|
||
zh: "快捷键已恢复默认",
|
||
en: "Shortcuts reset to defaults",
|
||
},
|
||
"status.shortcutSet": {
|
||
zh: ({ label, binding }) => `${localizedParam(label, "zh-CN")}已设置为 ${binding}`,
|
||
en: ({ label, binding }) => `${localizedParam(label, "en-US")} set to ${binding}`,
|
||
},
|
||
"status.rotateSpeed": {
|
||
zh: ({ speed }) => `旋转转速已设为 ${speed}`,
|
||
en: ({ speed }) => `Rotation speed set to ${speed}`,
|
||
},
|
||
"status.switchedTo": {
|
||
zh: ({ label }) => `已切换到${localizedParam(label, "zh-CN")}`,
|
||
en: ({ label }) => `Switched to ${localizedParam(label, "en-US")}`,
|
||
},
|
||
"status.viewReset": {
|
||
zh: "视角已重置",
|
||
en: "View reset",
|
||
},
|
||
"status.motionPrefix": {
|
||
zh: ({ text }) => `动捕: ${localizedParam(text, "zh-CN")}`,
|
||
en: ({ text }) => `Motion: ${localizedParam(text, "en-US")}`,
|
||
},
|
||
"status.motionCruiseTargetSwitched": {
|
||
zh: "动捕: 已切换巡航目标",
|
||
en: "Motion: switched cruise target",
|
||
},
|
||
"status.motionNoTarget": {
|
||
zh: ({ layer }) => (layer ? `动捕: ${localizedParam(layer, "zh-CN")}当前视野没有可选目标` : "动捕: 当前没有可用图层"),
|
||
en: ({ layer }) => (layer ? `Motion: ${localizedParam(layer, "en-US")} has no selectable targets in view` : "Motion: no available layer"),
|
||
},
|
||
"status.motionSwitchedTo": {
|
||
zh: ({ label, layer }) => `动捕: 已切换到${localizedParam(label, "zh-CN")}${layer ? "图层" : ""}`,
|
||
en: ({ label, layer }) => `Motion: switched to ${localizedParam(label, "en-US")}${layer ? " layer" : ""}`,
|
||
},
|
||
"status.motionNoVisibleLayer": {
|
||
zh: "动捕: 当前没有可切换的可见图层",
|
||
en: "Motion: no visible layer to switch to",
|
||
},
|
||
"status.motionConfirmed": {
|
||
zh: ({ label }) => `动捕: 已确认${localizedParam(label, "zh-CN")}`,
|
||
en: ({ label }) => `Motion: confirmed ${localizedParam(label, "en-US")}`,
|
||
},
|
||
"status.motionConfirmCurrent": {
|
||
zh: "动捕: 已确认当前目标",
|
||
en: "Motion: confirmed current target",
|
||
},
|
||
"status.motionSelectTargetFirst": {
|
||
zh: "动捕: 请先选择目标",
|
||
en: "Motion: select a target first",
|
||
},
|
||
"status.cruisePausedOpenCandidates": {
|
||
zh: "已暂停巡航,正在打开候选列表",
|
||
en: "Cruise paused; opening candidate list",
|
||
},
|
||
"status.motionPausedOpenCandidates": {
|
||
zh: "已暂停动捕目标展示,正在打开候选列表",
|
||
en: "Motion target display paused; opening candidate list",
|
||
},
|
||
"status.candidatesClosedCruiseResumed": {
|
||
zh: "候选列表已关闭,巡航已恢复",
|
||
en: "Candidate list closed; cruise resumed",
|
||
},
|
||
"status.cruiseModeRequired": {
|
||
zh: "切换到巡航模式后可切换卡片",
|
||
en: "Switch to Cruise Mode before switching cards",
|
||
},
|
||
"status.cruisePausedSpace": {
|
||
zh: "巡航已暂停,按空格恢复",
|
||
en: "Cruise paused. Press Space to resume.",
|
||
},
|
||
};
|
||
|
||
const EXACT_TRANSLATIONS = {
|
||
"智能星球计划 - 现实层宇宙全息感知": "Intelligent Planet - Reality Layer Situational Awareness",
|
||
"智能星球计划": "Intelligent Planet Program",
|
||
"现实层宇宙全息感知系统": "Reality Layer Situational Awareness System",
|
||
"卫星 · 海底光缆 · 算力基础设施": "Satellites · Subsea Cables · Compute Infrastructure",
|
||
"图层": "Layers",
|
||
"折叠": "Collapse",
|
||
"关闭": "Close",
|
||
"清除": "Clear",
|
||
"开启": "On",
|
||
"已显示": "Shown",
|
||
"已隐藏": "Hidden",
|
||
"折叠图层列表": "Collapse layer list",
|
||
"关闭图层面板": "Close layer panel",
|
||
"搜索图层...": "Search layers...",
|
||
"清除搜索": "Clear search",
|
||
"正在加载...": "Loading...",
|
||
"无匹配图层": "No matching layers",
|
||
"海缆": "Subsea Cables",
|
||
"卫星": "Satellites",
|
||
"算力中心": "Compute Centers",
|
||
"船只": "Vessels",
|
||
"地形": "Terrain",
|
||
"海陆基座": "Land/Ocean Base",
|
||
"高清材质": "HD Texture",
|
||
"大气云图": "Atmosphere & Clouds",
|
||
"国界线": "Borders",
|
||
"经纬线": "Graticule",
|
||
"切换海缆显示": "Toggle subsea cables",
|
||
"切换卫星显示": "Toggle satellites",
|
||
"切换算力中心显示": "Toggle compute centers",
|
||
"切换船只显示": "Toggle vessels",
|
||
"切换BGP观测显示": "Toggle BGP observations",
|
||
"BGP观测": "BGP Observations",
|
||
"BGP态势": "BGP Situation",
|
||
"观测站": "Collector",
|
||
"切换地形显示": "Toggle terrain",
|
||
"切换高清材质显示": "Toggle HD texture",
|
||
"切换大气云图显示": "Toggle atmosphere and clouds",
|
||
"切换国界显示": "Toggle borders",
|
||
"切换经纬线显示": "Toggle graticule",
|
||
"显示卫星": "Show Satellites",
|
||
"隐藏卫星": "Hide Satellites",
|
||
"显示船只": "Show Vessels",
|
||
"隐藏船只": "Hide Vessels",
|
||
"显示线缆": "Show Cables",
|
||
"隐藏线缆": "Hide Cables",
|
||
"显示经纬线": "Show Graticule",
|
||
"隐藏经纬线": "Hide Graticule",
|
||
"显示国界": "Show Borders",
|
||
"隐藏国界": "Hide Borders",
|
||
"显示国界线": "Show Border Lines",
|
||
"隐藏国界线": "Hide Border Lines",
|
||
"显示高清材质": "Show HD Texture",
|
||
"隐藏高清材质": "Hide HD Texture",
|
||
"显示大气云图": "Show Atmosphere & Clouds",
|
||
"隐藏大气云图": "Hide Atmosphere & Clouds",
|
||
"显示BGP观测": "Show BGP Observations",
|
||
"隐藏BGP观测": "Hide BGP Observations",
|
||
"显示算力中心": "Show Compute Centers",
|
||
"隐藏算力中心": "Hide Compute Centers",
|
||
"显示轨迹": "Show Trails",
|
||
"隐藏轨迹": "Hide Trails",
|
||
"显示地形": "Show Terrain",
|
||
"隐藏地形": "Hide Terrain",
|
||
"图层控制": "Layer Controls",
|
||
"搜索": "Search",
|
||
"自动旋转": "Auto Rotate",
|
||
"重新加载数据": "Reload Data",
|
||
"缩放控制": "Zoom Controls",
|
||
"缩放:": "Zoom:",
|
||
"放大": "Zoom In",
|
||
"缩小": "Zoom Out",
|
||
"设置": "Settings",
|
||
"重置视角": "Reset View",
|
||
"最大化布局": "Maximize Layout",
|
||
"工具菜单": "Tool Menu",
|
||
"展开工具菜单": "Open tool menu",
|
||
"展开图例": "Expand legend",
|
||
"折叠图例": "Collapse legend",
|
||
"关闭图例": "Close legend",
|
||
"关闭地球信息": "Close Earth info",
|
||
"Live 新闻": "Live News",
|
||
"选择新闻直播源": "Select live news source",
|
||
"刷新直播源": "Refresh live source",
|
||
"访问官网": "Visit website",
|
||
"折叠新闻直播内容": "Collapse live news details",
|
||
"关闭 Live 新闻面板": "Close Live News panel",
|
||
"暂无可用频道": "No Channel Available",
|
||
"待加载": "Pending",
|
||
"等待加载": "Waiting",
|
||
"等待加载直播源": "Waiting for live source",
|
||
"正在同步直播源...": "Syncing live sources...",
|
||
"直播已加载": "Live stream loaded",
|
||
"当前视频流不可播放,请尝试其他频道": "This stream cannot be played. Try another channel.",
|
||
"当前频道仅支持外部打开": "This channel opens externally only",
|
||
"电视直播源加载失败": "Failed to load TV streams",
|
||
"直播加载中": "Live stream loading",
|
||
"直播流缓冲停滞,正在等待数据": "Live stream stalled; waiting for data",
|
||
"直播流缓冲写入失败": "Failed to append live stream buffer",
|
||
"HLS 播放列表格式无法解析": "Unable to parse HLS playlist",
|
||
"HLS 分片格式无法解析": "Unable to parse HLS segment",
|
||
"HLS 播放流不可用": "HLS stream unavailable",
|
||
"直播放流不可用,已回退到官网直播页": "Live stream unavailable; falling back to the official live page",
|
||
"直播流网络波动,正在重试...": "Live stream network issue. Retrying...",
|
||
"直播流正在恢复...": "Live stream recovering...",
|
||
"已加载视频流,点击播放继续": "Video stream loaded. Click play to continue.",
|
||
"当前频道仅支持跳转官网或外部播放器打开。": "This channel can only open on the official site or an external player.",
|
||
"点击查看当前频道来源、目录和补充说明": "Tap to view channel source, catalog, and notes",
|
||
"点击查看完整频道信息": "Tap to view full channel info",
|
||
"展开新闻直播内容": "Expand live news details",
|
||
"展开新闻直播信息": "Expand live news details",
|
||
"折叠新闻直播信息": "Collapse live news details",
|
||
"Live 新闻窗口已打开": "Live News panel opened",
|
||
"Live 新闻窗口已关闭": "Live News panel closed",
|
||
"内置": "Built-in",
|
||
"内置源": "Built-in",
|
||
"采集": "Collected",
|
||
"采集源": "Collector source",
|
||
"频道": "Channel",
|
||
"尚未同步": "Not synced yet",
|
||
"最近同步": "Last synced",
|
||
"配置于控制台": "Configured in console",
|
||
"当前未配置可播放新闻直播源": "No playable live news source is configured",
|
||
"频道目录待同步": "Channel catalog pending sync",
|
||
"支持后台配置默认源与采集器补充源。": "Supports default channels and collector-supplied sources from the console.",
|
||
"暂无可播放直播源,请先在系统配置中添加频道。": "No playable live source. Add a channel in system configuration first.",
|
||
"打开态势新闻": "Open situation news",
|
||
"态势聚合新闻": "Situation News",
|
||
"刷新新闻源": "Refresh news sources",
|
||
"打开源站": "Open source site",
|
||
"收起态势新闻": "Collapse situation news",
|
||
"跟随地球正面视角自动切换区域新闻": "Region news follows the front-facing globe view",
|
||
"当前关注区域": "Current Focus",
|
||
"跟随当前视角聚焦全球区域新闻": "Focus regional news from the current view",
|
||
"类型": "Type",
|
||
"来源": "Source",
|
||
"查看全部": "View all",
|
||
"返回摘要": "Back to summary",
|
||
"当前区域新闻聚合中": "Aggregating regional news",
|
||
"正在同步全球态势新闻...": "Syncing global situation news...",
|
||
"正在准备全球态势新闻...": "Preparing global situation news...",
|
||
"当前新闻类型没有可显示新闻...": "No displayable news for the selected types...",
|
||
"当前区域暂无可用新闻": "No available news in this region",
|
||
"输入关键词以搜索当前地球对象": "Enter keywords to search current Earth objects",
|
||
"支持搜索海缆、登陆点、卫星、算力中心、BGP 事件与观测站。": "Search subsea cables, landing points, satellites, compute centers, BGP events, and collectors.",
|
||
"搜索海缆、登陆点、卫星、算力中心、BGP 事件...": "Search cables, landing points, satellites, compute centers, BGP events...",
|
||
"搜索海缆、登陆点、卫星、算力中心和 BGP 事件": "Search cables, landing points, satellites, compute centers, and BGP events",
|
||
"关闭搜索": "Close search",
|
||
"设置分类": "Settings categories",
|
||
"移动端设置分类": "Mobile settings categories",
|
||
"重置设置": "Reset settings",
|
||
"关闭设置": "Close settings",
|
||
"重置": "Reset",
|
||
"运行": "Runtime",
|
||
"显示": "Display",
|
||
"面板": "Panels",
|
||
"动捕": "Motion",
|
||
"快捷键": "Shortcuts",
|
||
"系统": "System",
|
||
"关于": "About",
|
||
"旋转模式": "Rotation Mode",
|
||
"巡航模式": "Cruise Mode",
|
||
"动捕模式": "Motion Mode",
|
||
"旋转、巡航和动捕是互斥运行模式": "Rotation, cruise, and motion are mutually exclusive runtime modes",
|
||
"旋转、巡航和动捕是互斥运行模式;巡航按模块轮播,动捕消费手势控制": "Rotation, cruise, and motion are mutually exclusive. Cruise rotates through modules; motion consumes gestures.",
|
||
"选择旋转模式": "Select rotation mode",
|
||
"移动端选择旋转模式": "Select rotation mode on mobile",
|
||
"旋转转速": "Rotation Speed",
|
||
"默认速率 1x": "Default speed 1x",
|
||
"默认速率 1x,调高后自动旋转更快。": "Default speed is 1x; increase it for faster auto rotation.",
|
||
"调整旋转转速": "Adjust rotation speed",
|
||
"移动端调整旋转转速": "Adjust rotation speed on mobile",
|
||
"重置旋转转速为1x": "Reset rotation speed to 1x",
|
||
"重置为1x": "Reset to 1x",
|
||
"巡航模块": "Cruise Modules",
|
||
"选择哪些可交互图层参与巡航队列。": "Choose which interactive layers join the cruise queue.",
|
||
"选择哪些可交互图层参与巡航队列。默认 BGP,其他图层按需加入。": "Choose which interactive layers join the cruise queue. BGP is default; add others as needed.",
|
||
"选择巡航模块": "Select cruise modules",
|
||
"移动端选择巡航模块": "Select cruise modules on mobile",
|
||
"新闻": "News",
|
||
"算力": "Compute",
|
||
"线缆": "Cables",
|
||
"轨迹": "Tracks",
|
||
"真实地形": "Real terrain",
|
||
"真实地形已显示": "Real terrain shown",
|
||
"电缆": "Cables",
|
||
"新闻类型": "News Types",
|
||
"自动旋转": "Auto Rotate",
|
||
"旋转": "Rotation",
|
||
"巡航": "Cruise",
|
||
"布局": "Layout",
|
||
"视角": "View",
|
||
"当前目标": "Current target",
|
||
"国界": "Borders",
|
||
"其他": "Other",
|
||
"巡航队列": "Cruise Queue",
|
||
"巡航大区顺序": "Cruise Region Order",
|
||
"控制巡航项按原始顺序、大区轮转或随机播放。": "Control whether cruise items play in original order, by region, or randomly.",
|
||
"控制巡航项按原始顺序、大区轮转或随机洗牌播放。": "Control whether cruise items play in original order, by region, or shuffled.",
|
||
"选择巡航队列模式": "Select cruise queue mode",
|
||
"移动端选择巡航队列模式": "Select cruise queue mode on mobile",
|
||
"默认": "Default",
|
||
"按大区": "By Region",
|
||
"随机": "Random",
|
||
"地球与地表": "Globe & Surface",
|
||
"日夜模式": "Day/Night Mode",
|
||
"按真实太阳位置区分地球昼夜明暗": "Use the real sun position for day/night shading",
|
||
"按真实太阳位置区分地球昼夜明暗,关闭后全球均匀照亮": "Use the real sun position for day/night shading; disabling evenly lights the globe.",
|
||
"地球默认大小": "Default Globe Size",
|
||
"用于重置视角、缩放重置和巡航视图": "Used for reset view, zoom reset, and cruise views",
|
||
"用于重置视角、缩放重置和巡航视图的默认缩放比例": "Default zoom for reset view, zoom reset, and cruise views",
|
||
"移动端调整地球默认大小": "Adjust default globe size on mobile",
|
||
"地形透明度": "Terrain Opacity",
|
||
"调高后会呈现更明显的绿色地形覆盖效果": "Higher values make the green terrain overlay more visible",
|
||
"移动端调整地形透明度": "Adjust terrain opacity on mobile",
|
||
"国界精度": "Boundary Precision",
|
||
"正在读取高清国界状态...": "Reading high-precision boundary status...",
|
||
"选择国界精度": "Select boundary precision",
|
||
"移动端选择国界精度": "Select boundary precision on mobile",
|
||
"低精": "Low",
|
||
"高精": "High",
|
||
"重新获取并构建高清国界": "Fetch and rebuild high-precision boundaries",
|
||
"重新获取并构建": "Fetch and rebuild",
|
||
"卫星显示风格": "Satellite Style",
|
||
"选择卫星锁定态使用自身发光或真实地表覆盖范围": "Choose self glow or true ground footprint for locked satellites",
|
||
"选择卫星锁定态使用自身发光,还是强调真实地表覆盖范围。": "Choose self glow for locked satellites or emphasize their true ground footprint.",
|
||
"选择卫星显示风格": "Select satellite display style",
|
||
"移动端选择卫星显示风格": "Select satellite display style on mobile",
|
||
"自身发光": "Self Glow",
|
||
"真实地表覆盖": "Ground Footprint",
|
||
"卫星呼吸闪烁": "Satellite Breathing",
|
||
"空闲时让卫星点缓慢明暗呼吸": "Let satellite dots breathe slowly while idle",
|
||
"空闲时让卫星点缓慢明暗呼吸,拖拽或缩放时自动稳定显示": "Let satellite dots breathe while idle; stabilize them while dragging or zooming.",
|
||
"真实卫星高度": "True Satellite Altitude",
|
||
"卫星真实高度": "True Satellite Altitude",
|
||
"旧版同层高度": "legacy shell altitude",
|
||
"按真实轨道高度压缩分层显示": "Compress and layer by true orbital altitude",
|
||
"按真实轨道高度压缩分层显示,关闭后使用旧版同层球面": "Compress and layer by true orbital altitude; disabling uses the legacy single shell.",
|
||
"轨迹显示": "Track Display",
|
||
"控制卫星轨迹线显示": "Control satellite track lines",
|
||
"控制卫星轨迹线显示,卫星图层关闭时会自动隐藏": "Control satellite track lines; hidden automatically when the satellite layer is off.",
|
||
"交互提示": "Interaction Hints",
|
||
"悬停提示": "Hover Hint",
|
||
"控制鼠标悬停地表时显示国家、位置或完整信息": "Choose whether surface hover shows country, position, or full details",
|
||
"控制鼠标悬停地表时显示国家、位置或完整信息。": "Choose whether surface hover shows country, position, or full details.",
|
||
"选择悬停提示内容": "Select hover hint content",
|
||
"移动端选择悬停提示内容": "Select hover hint content on mobile",
|
||
"国家": "Country",
|
||
"位置": "Position",
|
||
"完整": "Full",
|
||
"低缩放圆点": "Compact Dots",
|
||
"低缩放彩色圆点": "Compact color dots",
|
||
"150% 以下将可交互图标简化为对应颜色圆点": "Below 150%, simplify interactable icons to colored dots",
|
||
"150% 以下将可交互图标简化为对应颜色圆点,便于巡航和总览扫视": "Below 150%, simplify interactable icons to colored dots for cruise and overview scanning.",
|
||
"新闻类型": "News Types",
|
||
"只筛选当前浏览器的新闻面板与新闻巡航,不改变后台新闻源。": "Filters only this browser's news panel and news cruise; backend sources are unchanged.",
|
||
"选择新闻类型": "Select news types",
|
||
"政治": "Politics",
|
||
"商业": "Business",
|
||
"电商": "E-commerce",
|
||
"金融": "Finance",
|
||
"体育": "Sports",
|
||
"科技": "Technology",
|
||
"军事": "Military",
|
||
"灾害": "Disaster",
|
||
"能源": "Energy",
|
||
"社会": "Society",
|
||
"文化": "Culture",
|
||
"其他": "Other",
|
||
"其他电缆": "Other Cables",
|
||
"海缆系统": "Cable Systems",
|
||
"登陆点": "Landing Points",
|
||
"8K 卫星图": "8K Satellite",
|
||
"颗": "",
|
||
"个": "",
|
||
"艘": "",
|
||
"起": "",
|
||
"在轨卫星": "Orbiting Satellites",
|
||
"AIS 船只": "AIS Vessels",
|
||
"BGP 事件": "BGP Events",
|
||
"BGP 观测站": "BGP Collectors",
|
||
"运行中": "Running",
|
||
"当前无活跃事件": "No active events",
|
||
"AISStream 未连接": "AISStream Disconnected",
|
||
"AISStream 实时已连接 · 等待首批更新": "AISStream live connected · waiting for first updates",
|
||
"AISStream 正在重连": "AISStream reconnecting",
|
||
"AISStream 正在连接": "AISStream connecting",
|
||
"暂无可播放直播源": "No playable live source",
|
||
"打开 Live 新闻": "Open Live News",
|
||
"关闭 Live 新闻": "Close Live News",
|
||
"正在加载船只...": "Loading vessels...",
|
||
"正在加载登陆点...": "Loading landing points...",
|
||
"正在加载海缆...": "Loading subsea cables...",
|
||
"正在加载卫星...": "Loading satellites...",
|
||
"正在加载BGP态势...": "Loading BGP situation...",
|
||
"正在加载地球纹理...": "Loading Earth texture...",
|
||
"正在加载大气云图...": "Loading atmosphere and clouds...",
|
||
"正在加载海陆基座...": "Loading land/ocean base...",
|
||
"正在启用高清材质...": "Enabling HD texture...",
|
||
"正在加载算力中心...": "Loading compute centers...",
|
||
"正在加载线缆数据...": "Loading cable data...",
|
||
"正在加载卫星数据...": "Loading satellite data...",
|
||
"正在加载船只数据...": "Loading vessel data...",
|
||
"正在加载电缆数据...": "Loading cable data...",
|
||
"正在加载真实地形数据...": "Loading real terrain data...",
|
||
"正在渲染地形...": "Rendering terrain...",
|
||
"地形加载中...": "Terrain loading...",
|
||
"卫星加载中...": "Satellites loading...",
|
||
"国界加载中...": "Borders loading...",
|
||
"高清材质加载中...": "HD texture loading...",
|
||
"大气云图加载中...": "Atmosphere and clouds loading...",
|
||
"船只加载中...": "Vessels loading...",
|
||
"欢迎使用智能星球计划": "Welcome to Intelligent Planet",
|
||
"登录控制台并完成首次采集后,Earth 将显示实时数据层。": "After signing in to the console and completing the first collection, Earth will display live data layers.",
|
||
"后端服务在线": "Backend service online",
|
||
"登录控制台": "Sign in to Console",
|
||
"配置或确认数据源": "Configure or confirm datasources",
|
||
"触发首次采集": "Run the first collection",
|
||
"回到 Earth 查看结果": "Return to Earth to view results",
|
||
"概要": "Summary",
|
||
"当前记录": "Current Records",
|
||
"直播源": "Live Sources",
|
||
"活跃数据源": "Active Datasources",
|
||
"登录并采集数据": "Sign in and collect data",
|
||
"先浏览 Earth": "Browse Earth first",
|
||
"控制图层控制面板显示": "Control layer panel visibility",
|
||
"控制右侧图层控制面板显示": "Control the right-side layer panel",
|
||
"图例": "Legend",
|
||
"其他电缆": "Other Cables",
|
||
"赤道轨道": "Equatorial",
|
||
"低倾角轨道": "Low Incl.",
|
||
"中倾角轨道": "Mid Incl.",
|
||
"高倾角轨道": "High Incl.",
|
||
"逆行轨道": "Retrograde",
|
||
"赤道轨道(0-30°)": "Equatorial (0-30 deg)",
|
||
"低倾角轨道(30-60°)": "Low Incl. (30-60 deg)",
|
||
"中倾角轨道(60-90°)": "Mid Incl. (60-90 deg)",
|
||
"高倾角轨道(90-120°)": "High Incl. (90-120 deg)",
|
||
"逆行轨道(120-180°)": "Retrograde (120-180 deg)",
|
||
"低轨": "LEO",
|
||
"中轨": "MEO",
|
||
"高轨": "HEO",
|
||
"近地轨道": "LEO",
|
||
"低地球轨道": "LEO",
|
||
"中地球轨道": "MEO",
|
||
"高地球轨道": "HEO",
|
||
"同步轨道": "GSO",
|
||
"地球同步轨道": "GSO",
|
||
"静止轨道": "GEO",
|
||
"地球静止轨道": "GEO",
|
||
"太阳同步轨道": "SSO",
|
||
"极轨": "Polar",
|
||
"低轨(LEO)": "LEO",
|
||
"中轨(MEO)": "MEO",
|
||
"高轨(HEO)": "HEO",
|
||
"地球同步轨道(GSO)": "GSO",
|
||
"地球静止轨道(GEO)": "GEO",
|
||
"太阳同步轨道(SSO)": "SSO",
|
||
"极轨(Polar)": "Polar",
|
||
"陆地填色": "Land Fill",
|
||
"海洋填色": "Ocean Fill",
|
||
"货轮": "Cargo Ship",
|
||
"油轮": "Tanker",
|
||
"客船": "Passenger Ship",
|
||
"渔船": "Fishing Vessel",
|
||
"军舰": "Naval Vessel",
|
||
"停泊/低速": "Anchored / Slow",
|
||
"其他船只": "Other Vessels",
|
||
"中活跃观测站": "Medium-Activity Collector",
|
||
"高活跃观测站": "High-Activity Collector",
|
||
"观测范围示意": "Observation Coverage",
|
||
"事件连线 / 枢纽": "Event Links / Hubs",
|
||
"严重事件": "Critical Events",
|
||
"高危事件": "High-Risk Events",
|
||
"中危事件": "Medium-Risk Events",
|
||
"低危事件": "Low-Risk Events",
|
||
"控制图例面板显示": "Control legend panel visibility",
|
||
"控制左下角图例面板显示": "Control the lower-left legend panel",
|
||
"全球态势": "Global Situation",
|
||
"控制全球态势统计面板显示": "Control the global situation stats panel",
|
||
"控制右下角全球态势统计面板显示": "Control the lower-right global situation stats panel",
|
||
"新闻直播": "Live News",
|
||
"控制 Live 新闻面板显示": "Control the Live News panel",
|
||
"控制右上角 Live 新闻面板显示": "Control the upper-right Live News panel",
|
||
"动捕调试模式": "Motion Debug Mode",
|
||
"显示摄像头识别到的骨架连线和匹配动作": "Show detected skeleton lines and matched gestures",
|
||
"打开骨架连线面板;未匹配为红线,匹配动作后变绿": "Open the skeleton panel; unmatched lines are red and matched gestures turn green.",
|
||
"动捕输入源": "Motion Input",
|
||
"网页摄像头无需安装;Motion Agent 用于双摄或网络摄像头": "Browser camera needs no install; Motion Agent is for dual-camera or network-camera setups.",
|
||
"浏览器摄像头适合网页/SaaS;Motion Agent 适合双摄、RTSP/HTTP 和客户端": "Browser camera fits web/SaaS; Motion Agent fits dual cameras, RTSP/HTTP, and clients.",
|
||
"选择动捕输入源": "Select motion input",
|
||
"移动端选择动捕输入源": "Select motion input on mobile",
|
||
"浏览器摄像头": "Browser Camera",
|
||
"识别动作": "Recognized Gestures",
|
||
"关闭后不会触发对应星球控制": "Disabled gestures will not trigger Earth controls",
|
||
"未勾选的动作不会触发星球端控制;Motion Agent 会同步过滤这些动作": "Unchecked gestures will not trigger Earth controls; Motion Agent filters them too.",
|
||
"选择动捕识别动作": "Select recognized gestures",
|
||
"移动端选择动捕识别动作": "Select recognized gestures on mobile",
|
||
"左旋": "Rotate Left",
|
||
"右旋": "Rotate Right",
|
||
"上旋": "Rotate Up",
|
||
"下旋": "Rotate Down",
|
||
"上个焦点": "Previous Focus",
|
||
"下个焦点": "Next Focus",
|
||
"上一图层": "Previous Layer",
|
||
"下一图层": "Next Layer",
|
||
"确认": "Confirm",
|
||
"键盘控制": "Keyboard Controls",
|
||
"点击按键后按下新的快捷键;Esc 取消录入": "Click a key, then press the new shortcut. Esc cancels capture.",
|
||
"点击按键后按下新的快捷键;Esc 取消录入,冲突快捷键不会保存": "Click a key, then press the new shortcut. Esc cancels capture; conflicts are not saved.",
|
||
"恢复默认快捷键": "Restore Default Shortcuts",
|
||
"恢复默认": "Restore Defaults",
|
||
"控制台": "Console",
|
||
"打开数据源、任务和系统运维工作台": "Open the datasource, task, and operations workspace",
|
||
"文档": "Docs",
|
||
"查看使用手册与开发文档": "Read manuals and developer docs",
|
||
"语言": "Language",
|
||
"星球语言": "Earth Language",
|
||
"同步 Docs 和控制台语言偏好": "Syncs with Docs and console language preference",
|
||
"中文": "中文",
|
||
"打开控制台": "Open Console",
|
||
"访问": "Open",
|
||
"查看": "View",
|
||
"仅保留移动端仍有意义的智能星球配置": "Keeps only Earth settings that still matter on mobile",
|
||
"动作捕捉": "Motion Capture",
|
||
"查看浏览器摄像头画面、骨架连线和当前匹配动作": "View browser camera, skeleton lines, and current matched gesture",
|
||
"动捕调试画面已并入设置里的动捕模式。": "Motion debug view has moved into Motion Mode in settings.",
|
||
"频道信息": "Channel Info",
|
||
"展开查看当前频道来源、目录和补充说明": "Expand to view current channel source, catalog, and notes",
|
||
"频道摘要标签": "Channel summary tags",
|
||
"刷新频道列表": "Refresh channel list",
|
||
"访问频道官网": "Visit channel website",
|
||
"移动端新闻直播": "Mobile live news",
|
||
"移动端新闻直播和频道切换": "Mobile live news and channel switching",
|
||
"Details": "Details",
|
||
"点击地球对象后查看统一详情": "Select an Earth object to view unified details",
|
||
"对象详情": "Object Details",
|
||
"等待选择对象": "Waiting for selection",
|
||
"点击海缆、算力中心、BGP 事件或卫星后在这里查看详情。": "Select a cable, compute center, BGP event, or satellite to view details here.",
|
||
"出品方": "Producer",
|
||
"策划人": "Planners",
|
||
"产品兼开发者": "Product & Development",
|
||
"浙江大学临空智能媒体研究院": "Zhejiang University Linkong Institute of Intelligent Media",
|
||
"方兴东、黄柳青": "Fang Xingdong, Huang Liuqing",
|
||
"钱坤、张鸽、齐鹏": "Qian Kun, Zhang Ge, Qi Peng",
|
||
"面向临空场景下的智能媒体研究、全球态势感知与多源开放数据巡航,提供可视化观测、事件聚合与交互式探索能力。": "For intelligent media research, global situational awareness, and multi-source open-data cruise in linkong scenarios, providing visual observation, event aggregation, and interactive exploration.",
|
||
"暂无": "None",
|
||
"未选": "None selected",
|
||
"全部": "All",
|
||
"区域监测": "Regional Monitor",
|
||
"聚合源": "Aggregated Source",
|
||
"手动添加": "Manual",
|
||
"单源 Atom": "Single-source Atom",
|
||
"配置保留": "Reserved",
|
||
"单源 RSS": "Single-source RSS",
|
||
"关注": "Watch",
|
||
"突发": "Breaking",
|
||
"严重突发": "Critical Breaking",
|
||
"区域": "Regional",
|
||
"大洲": "Continent",
|
||
"亚洲": "Asia",
|
||
"欧洲": "Europe",
|
||
"非洲": "Africa",
|
||
"北美洲": "North America",
|
||
"南美洲": "South America",
|
||
"大洋洲": "Oceania",
|
||
"南极洲": "Antarctica",
|
||
"全球": "Global",
|
||
"美洲": "Americas",
|
||
"欧洲": "Europe",
|
||
"中东与非洲": "Middle East & Africa",
|
||
"亚太": "Asia Pacific",
|
||
"全球焦点": "Global Focus",
|
||
"源类型": "Source type",
|
||
"抓取通道": "Fetch channel",
|
||
"新闻来源": "News Sources",
|
||
"按大来源筛选,不影响后台抓取。": "Filter by source group; backend fetching is unchanged.",
|
||
"按新闻内容分类筛选。": "Filter by news content category.",
|
||
"暂无可筛选来源。": "No filterable sources.",
|
||
"当前区域暂无可用新闻,已完成一次聚合尝试": "No available news in this region; one aggregation attempt completed",
|
||
"当前未拉到可用新闻,请稍后刷新或切换视角区域。": "No available news yet. Refresh later or switch view region.",
|
||
"当前新闻类型没有可显示新闻。": "No displayable news for the selected types.",
|
||
"等待聚合新闻源": "Waiting for aggregated news sources",
|
||
"正在准备全球态势新闻聚合源...": "Preparing global situation news sources...",
|
||
"跟随当前视角自动聚焦": "Follows the current view automatically",
|
||
"刚刚同步": "Just synced",
|
||
"新闻聚合请求超时,请稍后重试": "News aggregation timed out. Try again later.",
|
||
"新闻聚合暂时不可用": "News aggregation is temporarily unavailable",
|
||
"态势新闻同步失败": "Failed to sync situation news",
|
||
"态势新闻已刷新": "Situation news refreshed",
|
||
"态势新闻刷新失败": "Failed to refresh situation news",
|
||
"快捷键已恢复默认": "Shortcuts restored to defaults",
|
||
"布局已最大化": "Layout maximized",
|
||
"布局已恢复": "Layout restored",
|
||
"视角已重置": "View reset",
|
||
"数据已加载": "Data loaded",
|
||
"巡航已暂停,按空格恢复": "Cruise paused. Press Space to resume.",
|
||
"巡航大区顺序已更新": "Cruise region order updated",
|
||
"候选列表已关闭,巡航已恢复": "Candidate list closed; cruise resumed",
|
||
"已暂停巡航,正在打开候选列表": "Cruise paused; opening candidate list",
|
||
"已暂停动捕目标展示,正在打开候选列表": "Motion target display paused; opening candidate list",
|
||
"切换到巡航模式后可切换卡片": "Switch to Cruise Mode to change cards",
|
||
"动捕识别动作已更新": "Motion gestures updated",
|
||
"动捕调试模式已开启": "Motion debug mode enabled",
|
||
"动捕调试模式已关闭": "Motion debug mode disabled",
|
||
"动捕调试模式将在下次进入动捕时开启": "Motion debug mode will enable next time Motion Mode starts",
|
||
"动捕调试已切换为只显示骨骼": "Motion debug switched to skeleton only",
|
||
"动捕调试已显示实时画面": "Motion debug now shows live video",
|
||
"动捕输入源已切换为 Motion Agent": "Motion input switched to Motion Agent",
|
||
"动捕输入源已切换为浏览器摄像头": "Motion input switched to browser camera",
|
||
"动捕: 已切换巡航目标": "Motion: cruise target changed",
|
||
"动捕: 当前没有可用图层": "Motion: no available layers",
|
||
"动捕: 当前没有可切换的可见图层": "Motion: no switchable visible layers",
|
||
"动捕: 已确认当前目标": "Motion: current target confirmed",
|
||
"动捕: 请先选择目标": "Motion: select a target first",
|
||
"Earth 设置已重置": "Earth settings reset",
|
||
"按键...": "Press key...",
|
||
"启用快捷键": "Enable shortcut",
|
||
"视角控制": "View Controls",
|
||
"工具": "Tools",
|
||
"向上旋转": "Rotate Up",
|
||
"向左旋转": "Rotate Left",
|
||
"向下旋转": "Rotate Down",
|
||
"向右旋转": "Rotate Right",
|
||
"关闭当前焦点菜单": "Close Current Focus Menu",
|
||
"打开搜索": "Open Search",
|
||
"打开/关闭图层面板": "Toggle Layer Panel",
|
||
"打开/关闭新闻直播": "Toggle Live News",
|
||
"暂停/恢复运行": "Pause/Resume Runtime",
|
||
"下一张巡航卡片": "Next Cruise Card",
|
||
"切换海缆": "Toggle Cables",
|
||
"切换卫星": "Toggle Satellites",
|
||
"切换算力中心": "Toggle Compute Centers",
|
||
"切换船只": "Toggle Vessels",
|
||
"切换 BGP观测": "Toggle BGP Observations",
|
||
"切换地形": "Toggle Terrain",
|
||
"切换高清材质": "Toggle HD Texture",
|
||
"切换大气云图": "Toggle Atmosphere & Clouds",
|
||
"切换国界线": "Toggle Borders",
|
||
"切换经纬线": "Toggle Graticule",
|
||
"卫星关闭时不可用": "Unavailable while Satellites are disabled",
|
||
"高清材质关闭时不可用": "Unavailable while HD texture is disabled",
|
||
"切换到动捕模式后可开启调试面板": "Switch to Motion Mode to enable the debug panel",
|
||
"卫星已切换为旧版同层高度": "Satellites switched to legacy same-layer altitude",
|
||
"关闭动捕调试面板": "Close motion debug panel",
|
||
"重置缩放到100%": "Reset zoom to 100%",
|
||
"移动端菜单": "Mobile menu",
|
||
"输入名称、地点、NORAD、ASN...": "Search by name, location, NORAD, ASN...",
|
||
"移动端搜索结果": "Mobile search results",
|
||
"选择移动端新闻直播源": "Select mobile live news source",
|
||
"搜索结果": "Search results",
|
||
"调整地球默认大小": "Adjust default Earth size",
|
||
"调整地形透明度": "Adjust terrain opacity",
|
||
"浙江大学临空智能媒体研究院 Logo": "Zhejiang University Linkong Institute of Intelligent Media Logo",
|
||
"Earth 页面发生未捕获错误": "Earth page uncaught error",
|
||
"Earth 页面发生未处理 Promise 错误": "Earth page unhandled Promise error",
|
||
"等待动捕数据": "Waiting for motion data",
|
||
"当前浏览器不支持 WebSocket": "This browser does not support WebSocket",
|
||
"无法创建 Motion Agent 连接": "Unable to create Motion Agent connection",
|
||
"Motion Agent 连接异常": "Motion Agent connection error",
|
||
"Motion Agent 未连接": "Motion Agent disconnected",
|
||
"演示模式已开启": "Demo mode enabled",
|
||
"新闻汉化待重试": "News localization pending retry",
|
||
"已汉化": "Localized",
|
||
"未知线缆": "Unknown Cable",
|
||
"未知登陆点": "Unknown Landing Point",
|
||
"未知节点": "Unknown Node",
|
||
"未知目标": "Unknown Target",
|
||
"未知错误": "Unknown error",
|
||
"请检查更新源": "Check update source",
|
||
"构建失败": "Build failed",
|
||
"已选择:": "Selected:",
|
||
"海底光缆系统": "Subsea Cable System",
|
||
"后端没有返回 JSON 状态": "Backend did not return JSON status",
|
||
"正在准备高清国界": "Preparing high-precision boundaries",
|
||
"当前使用高精国界;可重新获取并构建。": "Using high-precision boundaries; you can fetch and rebuild.",
|
||
"高精国界已就绪,切到高精会立即应用。": "High-precision boundaries are ready and can be applied immediately.",
|
||
"当前使用低精国界;切到高精会下载并构建。": "Using low-precision boundaries; switching to high will download and build.",
|
||
"高精国界已下载并应用": "High-precision boundaries downloaded and applied",
|
||
"正在启动高精国界构建": "Starting high-precision boundary build",
|
||
"高精国界构建已启动": "High-precision boundary build started",
|
||
"已切换到高精国界": "Switched to high-precision boundaries",
|
||
"高清国界进度读取失败": "Failed to read high-precision boundary progress",
|
||
"高清国界状态读取失败": "Failed to read high-precision boundary status",
|
||
"高精国界切换失败": "Failed to switch high-precision boundaries",
|
||
"高精国界重建启动失败": "Failed to start high-precision boundary rebuild",
|
||
"低精国界切换失败": "Failed to switch low-precision boundaries",
|
||
"已切换到低精国界": "Switched to low-precision boundaries",
|
||
"详情": "Details",
|
||
"关闭详情": "Close details",
|
||
"电缆详情": "Cable Details",
|
||
"登陆点详情": "Landing Point Details",
|
||
"卫星详情": "Satellite Details",
|
||
"BGP事件详情": "BGP Event Details",
|
||
"新闻事件详情": "News Event Details",
|
||
"BGP观测站详情": "BGP Collector Details",
|
||
"超算中心详情": "Supercomputer Details",
|
||
"GPU集群详情": "GPU Cluster Details",
|
||
"船只详情": "Vessel Details",
|
||
"交互点详情": "Interactable Details",
|
||
"交互点": "Interactable",
|
||
"数据点": "Data Point",
|
||
"名称": "Name",
|
||
"所有者": "Owner",
|
||
"状态": "Status",
|
||
"长度": "Length",
|
||
"经纬度": "Coordinates",
|
||
"海拔": "Altitude",
|
||
"投入使用": "Ready for Service",
|
||
"关联海缆数": "Related Cable Count",
|
||
"关联海缆": "Related Cables",
|
||
"星座/分组": "Constellation / Group",
|
||
"覆盖能力": "Coverage Capability",
|
||
"当前显示": "Current Display",
|
||
"覆盖模型": "Coverage Model",
|
||
"倾角": "Inclination",
|
||
"周期": "Period",
|
||
"分钟": "min",
|
||
"近地点高度": "Perigee Altitude",
|
||
"远地点高度": "Apogee Altitude",
|
||
"事件类型": "Event Type",
|
||
"严重度": "Severity",
|
||
"事件特征": "Event Feature",
|
||
"前缀": "Prefix",
|
||
"传播路径": "AS Path",
|
||
"涉及 ASN": "Origin ASN",
|
||
"关联 ASN": "Related ASN",
|
||
"置信度": "Confidence",
|
||
"主观测站": "Primary Collector",
|
||
"观测范围": "Observation Scope",
|
||
"影响区域": "Impact Region",
|
||
"附近基础设施": "Nearby Infrastructure",
|
||
"附近卫星": "Nearby Satellites",
|
||
"观测位置": "Observation Location",
|
||
"事件时间": "Event Time",
|
||
"摘要": "Summary",
|
||
"媒体来源": "Media Source",
|
||
"source": "Source",
|
||
"name": "Name",
|
||
"country": "Country",
|
||
"country_code": "Country Code",
|
||
"flag": "Flag",
|
||
"type": "Type",
|
||
"vessel_type": "Vessel Type",
|
||
"width": "Width",
|
||
"gross_tonnage": "Gross Tonnage",
|
||
"deadweight": "Deadweight",
|
||
"built_year": "Built Year",
|
||
"operator": "Operator",
|
||
"owner": "Owner",
|
||
"status": "Status",
|
||
"last_updated": "Last Updated",
|
||
"发布时间": "Published",
|
||
"发生地": "Location",
|
||
"原文链接": "Source Link",
|
||
"采集器": "Collector",
|
||
"当前事件数": "Current Events",
|
||
"观测事件数": "Observed Events",
|
||
"近24h事件数": "24h Events",
|
||
"近7d事件数": "7d Events",
|
||
"观测前缀数": "Observed Prefixes",
|
||
"观测 ASN 数": "Observed ASNs",
|
||
"主要事件类型": "Top Event Types",
|
||
"日常活跃度": "Daily Activity",
|
||
"最近事件类型": "Latest Event Type",
|
||
"最近活跃时间": "Latest Activity",
|
||
"日常覆盖范围": "Baseline Scope",
|
||
"类型": "Type",
|
||
"排名": "Rank",
|
||
"实测算力": "Measured Compute",
|
||
"估算算力": "Estimated Compute",
|
||
"厂商": "Vendor",
|
||
"运营方": "Operator",
|
||
"核心数": "Cores",
|
||
"功耗": "Power",
|
||
"城市": "City",
|
||
"位置精度": "Location Precision",
|
||
"位置来源": "Location Source",
|
||
"位置置信度": "Location Confidence",
|
||
"核验状态": "Verification",
|
||
"解析依据": "Evidence",
|
||
"位置来源说明": "Location Source Note",
|
||
"匹配的位置名称": "Matched Location",
|
||
"位置核验时间": "Verified At",
|
||
"更新时间": "Updated",
|
||
"GPU 数量": "GPU Count",
|
||
"GPU 型号": "GPU Model",
|
||
"芯片/平台": "Chip / Platform",
|
||
"旗帜": "Flag",
|
||
"船型": "Vessel Type",
|
||
"当前航速": "Speed",
|
||
"航向": "Course",
|
||
"船长": "Length",
|
||
"标识": "ID",
|
||
"纬度": "Latitude",
|
||
"经度": "Longitude",
|
||
"说明": "Description",
|
||
"字段来源": "Field Source",
|
||
"新闻信号": "News Signal",
|
||
"船舶资料": "Vessel Profile",
|
||
"资料缓存中": "Profile caching",
|
||
"资料": "Profile",
|
||
"媒体": "Media",
|
||
"无可复制内容": "Nothing to copy",
|
||
"复制失败": "Copy failed",
|
||
"未知来源": "Unknown Source",
|
||
"不适用": "N/A",
|
||
"Starlink 单星地表覆盖": "Starlink single-satellite ground footprint",
|
||
"Iridium 外圈半透明覆盖": "Iridium translucent outer coverage ring",
|
||
"锚泊": "At anchor",
|
||
"停靠": "Moored",
|
||
"航行中": "Under way",
|
||
"重新自动采集坐标": "Recollect Coordinates",
|
||
"自动采集坐标候选": "Collect Coordinate Candidates",
|
||
"采集坐标候选": "Collect coordinate candidates",
|
||
"一键定位并采用最高置信候选": "Locate all and apply highest-confidence candidates",
|
||
"一键定位": "Locate All",
|
||
"采集": "Collected",
|
||
"正在检查 WebSearch 状态,稍候即可定位。": "Checking WebSearch status; location will be available shortly.",
|
||
"WebSearch 未开启,无法进行事实核查定位。": "WebSearch is disabled, so fact-checked location is unavailable.",
|
||
"精确": "Precise",
|
||
"站点": "Site",
|
||
"候选": "Candidate",
|
||
"预览": "Preview",
|
||
"保存": "Save",
|
||
"当前没有待定位算力中心": "No compute centers need location",
|
||
"缺少可用地址字段": "Missing usable address fields",
|
||
"待定位算力中心": "Compute Centers Pending Location",
|
||
"未命名算力中心": "Unnamed Compute Center",
|
||
"超级计算机": "Supercomputer",
|
||
"GPU集群": "GPU Cluster",
|
||
"BGP路由异常": "BGP Route Anomaly",
|
||
"态势新闻": "Situation News",
|
||
"超算": "Supercomputer",
|
||
"超算中心": "Supercomputer",
|
||
"GPU 集群": "GPU Cluster",
|
||
"BGP事件": "BGP Event",
|
||
"未知": "Unknown",
|
||
"未知国家": "Unknown Country",
|
||
"未知海缆": "Unknown Cable",
|
||
"未知卫星": "Unknown Satellite",
|
||
"未知船只": "Unknown Vessel",
|
||
"未知观测站": "Unknown Collector",
|
||
"未知算力中心": "Unknown Compute Center",
|
||
"精确坐标": "Precise Coordinates",
|
||
"站点级位置": "Site-Level Location",
|
||
"城市级位置": "City-Level Location",
|
||
"位置未确认": "Location Unconfirmed",
|
||
"源数据自带坐标": "Source Coordinates",
|
||
"ROR 组织注册 API": "ROR Registry API",
|
||
"Nominatim 在线搜索": "Nominatim Online Search",
|
||
"LLM factcheck 兜底": "LLM Factcheck Fallback",
|
||
"待人工核验": "Needs Review",
|
||
"估算位置": "Estimated Location",
|
||
"已确认": "Confirmed",
|
||
"严重": "Critical",
|
||
"高": "High",
|
||
"中": "Medium",
|
||
"低": "Low",
|
||
"前缀劫持": "Prefix Hijack",
|
||
"路由泄露": "Route Leak",
|
||
"大规模撤销": "Mass Withdrawal",
|
||
"更具体前缀异常": "More-Specific Prefix Anomaly",
|
||
"路径突变": "Path Change",
|
||
"路由抖动": "Route Flap",
|
||
"活跃": "Active",
|
||
"已恢复": "Resolved",
|
||
"已抑制": "Suppressed",
|
||
"在线": "Online",
|
||
"离线": "Offline",
|
||
"静态观测站": "Static Collector",
|
||
"附近登陆点": "Nearby Landing Point",
|
||
"支持 Starlink 地表覆盖": "Starlink ground footprint supported",
|
||
"支持 Iridium 外圈覆盖": "Iridium outer ring supported",
|
||
"默认不显示 footprint": "Footprint hidden by default",
|
||
"真实地表覆盖(Starlink)": "Ground Footprint (Starlink)",
|
||
"真实地表覆盖(Iridium 外圈)": "Ground Footprint (Iridium outer ring)",
|
||
"中国": "China",
|
||
"美国": "United States",
|
||
"英国": "United Kingdom",
|
||
"法国": "France",
|
||
"德国": "Germany",
|
||
"日本": "Japan",
|
||
"韩国": "South Korea",
|
||
"新加坡": "Singapore",
|
||
"印度": "India",
|
||
"俄罗斯": "Russia",
|
||
"加拿大": "Canada",
|
||
"澳大利亚": "Australia",
|
||
"荷兰": "Netherlands",
|
||
"瑞士": "Switzerland",
|
||
"意大利": "Italy",
|
||
"西班牙": "Spain",
|
||
"瑞典": "Sweden",
|
||
"挪威": "Norway",
|
||
"芬兰": "Finland",
|
||
"丹麦": "Denmark",
|
||
"巴西": "Brazil",
|
||
"墨西哥": "Mexico",
|
||
"南非": "South Africa",
|
||
"阿联酋": "United Arab Emirates",
|
||
"沙特阿拉伯": "Saudi Arabia",
|
||
"以色列": "Israel",
|
||
"中国台湾": "Taiwan",
|
||
"台湾": "Taiwan",
|
||
"中国香港": "Hong Kong",
|
||
"香港": "Hong Kong",
|
||
"正在初始化...": "Initializing...",
|
||
"算力中心坐标已保存": "Compute center coordinates saved",
|
||
"坐标已保存,地图稍后同步": "Coordinates saved; map will sync shortly",
|
||
"坐标已保存,正在同步地图...": "Coordinates saved; syncing map...",
|
||
};
|
||
|
||
const PATTERN_TRANSLATIONS = [
|
||
[/^切换(.+)显示$/, (match, locale) => `Toggle ${translateText(match[1], locale)} display`],
|
||
[/^(\d+) 项$/, "$1"],
|
||
[/^(\d+) 条$/, "$1"],
|
||
[/^(\d+)个$/, "$1"],
|
||
[/^(\d+) 个$/, "$1"],
|
||
[/^(\d+) 颗$/, "$1"],
|
||
[/^(\d+)颗$/, "$1"],
|
||
[/^(\d+) 艘$/, "$1"],
|
||
[/^(\d+) 起$/, "$1"],
|
||
[/^(\d+)\/(\d+) 运行中$/, "$1/$2 running"],
|
||
[/^(\d+) 起活跃事件$/, "$1 active events"],
|
||
[/^(\d+) 起活跃事件 \/ (\d+) 条异常$/, "$1 active events / $2 anomalies"],
|
||
[/^(\d+) 条活跃异常$/, "$1 active anomalies"],
|
||
[/^(.+) \((\d+) pending location\)$/, (match, locale) => `${translateText(match[1], locale)} (${match[2]} pending location)`],
|
||
[/^(.+) · (.+)$/, (match, locale) => `${translateText(match[1], locale)} · ${translateText(match[2], locale)}`],
|
||
[/^暂停(.+)$/, (match, locale) => `Pause ${translateText(match[1], locale)}`],
|
||
[/^开始(.+)$/, (match, locale) => `Start ${translateText(match[1], locale)}`],
|
||
[/^共 (\d+) 个频道 · 尚未同步$/, "$1 channels · Not synced yet"],
|
||
[/^共 (\d+) 个频道 · 最近同步 (.+)$/, "$1 channels · Last synced $2"],
|
||
[/^采集源:(.+)$/, "Collector source: $1"],
|
||
[/^采集:(.+)$/, "Collector: $1"],
|
||
[/^成功加载 (\d+) 条电缆$/, "Loaded $1 cables"],
|
||
[/^成功加载 (\d+) 个登陆点$/, "Loaded $1 landing points"],
|
||
[/^已锁定: (.+)$/, "Locked: $1"],
|
||
[/^已启用 (\d+) 个图层$/, "$1 layers enabled"],
|
||
[/^HLS 主播放列表加载失败(HTTP (\d+))$/, "Failed to load HLS master playlist (HTTP $1)"],
|
||
[/^HLS 清晰度播放列表加载失败(HTTP (\d+))$/, "Failed to load HLS variant playlist (HTTP $1)"],
|
||
[/^HLS 分片加载失败(HTTP (\d+))$/, "Failed to load HLS segment (HTTP $1)"],
|
||
[/^HLS 主播放列表加载失败$/, "Failed to load HLS master playlist"],
|
||
[/^HLS 清晰度播放列表加载失败$/, "Failed to load HLS variant playlist"],
|
||
[/^HLS 分片加载失败$/, "Failed to load HLS segment"],
|
||
[/^HLS 资源加载失败:(.+)$/, "Failed to load HLS resource: $1"],
|
||
[/^直播流连接异常,正在重试 \((\d+)\/(\d+)\)\.\.\.$/, "Live stream connection issue. Retrying ($1/$2)..."],
|
||
[/^直播流解码异常,正在恢复 \((\d+)\/(\d+)\)\.\.\.$/, "Live stream decode issue. Recovering ($1/$2)..."],
|
||
[/^失败:(.*)$/, "Failed: $1"],
|
||
[/^高清国界更新源未配置完整:(.*)$/, "High-precision boundary sources are incomplete: $1"],
|
||
[/^高清国界下载失败:(.*)$/, "High-precision boundary download failed: $1"],
|
||
[/^高清国界进度读取失败:(.*)$/, "Failed to read high-precision boundary progress: $1"],
|
||
[/^高清国界状态读取失败:(.*)$/, "Failed to read high-precision boundary status: $1"],
|
||
[/^高精国界切换失败:(.*)$/, "Failed to switch high-precision boundaries: $1"],
|
||
[/^高精国界重建启动失败:(.*)$/, "Failed to start high-precision boundary rebuild: $1"],
|
||
[/^低精国界切换失败:(.*)$/, "Failed to switch low-precision boundaries: $1"],
|
||
[/^快捷键已被「(.+)」使用$/, "Shortcut is already used by \"$1\""],
|
||
[/^(.+)已显示$/, (match, locale) => `${translateText(match[1], locale)} shown`],
|
||
[/^(.+)已隐藏$/, (match, locale) => `${translateText(match[1], locale)} hidden`],
|
||
[/^(.+)已启用$/, (match, locale) => `${translateText(match[1], locale)} enabled`],
|
||
[/^(.+)已开启$/, (match, locale) => `${translateText(match[1], locale)} enabled`],
|
||
[/^(.+)已关闭$/, (match, locale) => `${translateText(match[1], locale)} disabled`],
|
||
[/^(.+)已暂停$/, (match, locale) => `${translateText(match[1], locale)} paused`],
|
||
[/^(.+)已恢复$/, (match, locale) => `${translateText(match[1], locale)} resumed`],
|
||
[/^(.+)当前不可用$/, (match, locale) => `${translateText(match[1], locale)} unavailable`],
|
||
[/^(.+)暂时不可用$/, (match, locale) => `${translateText(match[1], locale)} temporarily unavailable`],
|
||
[/^正在加载(.+)数据\.\.\.$/, (match, locale) => `Loading ${translateText(match[1], locale).toLowerCase()} data...`],
|
||
[/^正在加载(.+)\.\.\.$/, (match, locale) => `Loading ${translateText(match[1], locale).toLowerCase()}...`],
|
||
[/^(.+)加载失败: (.+)$/, (match, locale) => `${translateText(match[1], locale)} load failed: ${match[2]}`],
|
||
[/^已定位(.+):(.+)$/, (match, locale) => `Located ${translateText(match[1], locale).toLowerCase()}: ${match[2]}`],
|
||
[/^已选择(.+): (.+)$/, (match, locale) => `Selected ${translateText(match[1], locale).toLowerCase()}: ${match[2]}`],
|
||
[/^(.+)已切换为:(.+)$/, (match, locale) => `${translateText(match[1], locale)} switched to: ${translateText(match[2], locale)}`],
|
||
[/^(.+)已设置为 (.+)$/, "$1 set to $2"],
|
||
[/^(.+)已设为 (.+)$/, (match, locale) => `${translateText(match[1], locale)} set to ${translateText(match[2], locale)}`],
|
||
[/^重置(.+)$/, "Reset $1"],
|
||
[/^已切换到(.+)$/, (match, locale) => `Switched to ${translateText(match[1], locale)}`],
|
||
[/^最近同步 (.+)$/, "Last synced $1"],
|
||
[/^缩放 (\d+)%$/, "Zoom $1%"],
|
||
[/^AISStream 实时已连接 · (\d+) 次更新$/, "AISStream live connected · $1 updates"],
|
||
[/^AISStream 实时已连接 · (\d+) 次更新 · (.+)$/, "AISStream live connected · $1 updates · $2"],
|
||
[/^AISStream 后台已连接$/, "AISStream backend connected"],
|
||
[/^AISStream 后台已连接 · 最近 (.+)$/, "AISStream backend connected · last seen $1"],
|
||
[/^(\d+) 秒前$/, "$1s ago"],
|
||
[/^(\d+) 分钟前$/, "$1m ago"],
|
||
[/^已选择BGP事件: (.+) · (\d+)个区域 \/ (\d+)条相关海缆$/, "Selected BGP event: $1 · $2 regions / $3 related cables"],
|
||
[/^已选择观测站: (.+)$/, "Selected collector: $1"],
|
||
[/^新闻源请求失败: (\d+)$/, "News request failed: $1"],
|
||
[/^来源 (.+)$/, "Source $1"],
|
||
[/^更新 (.+)$/, "Updated $1"],
|
||
[/^置信 (.+)$/, "Confidence $1"],
|
||
[/^已复制(.+):(.+)$/, "Copied $1: $2"],
|
||
[/^已选择: (.+)$/, "Selected: $1"],
|
||
[/^动捕: (.+)当前视野没有可选目标$/, (match, locale) => `Motion: ${translateText(match[1], locale)} has no selectable targets in view`],
|
||
[/^动捕: 已切换到(.+)图层$/, (match, locale) => `Motion: switched to ${translateText(match[1], locale)} layer`],
|
||
[/^动捕: 已切换到(.+)$/, (match, locale) => `Motion: switched to ${translateText(match[1], locale)}`],
|
||
[/^动捕: 已确认(.+)$/, (match, locale) => `Motion: confirmed ${translateText(match[1], locale)}`],
|
||
[/^共找到 (\d+) 个候选位置$/, "Found $1 candidate locations"],
|
||
[/^(\d+) 个算力中心没有可信坐标$/, "$1 compute centers lack trusted coordinates"],
|
||
[/^(\d+) 个待定位$/, "$1 pending location"],
|
||
[/^正在定位并采用最高置信候选 (\d+)\/(\d+)\.\.\.$/, "Locating and applying highest-confidence candidate $1/$2..."],
|
||
[/^一键定位进行中 (\d+)\/(\d+)\.\.\.$/, "Batch location in progress $1/$2..."],
|
||
[/^找到 (\d+) 个候选,正在保存最高置信位置\.\.\.$/, "Found $1 candidates; saving highest-confidence location..."],
|
||
[/^坐标已保存,等待图层刷新$/, "Coordinates saved; waiting for layer refresh"],
|
||
[/^坐标已保存$/, "Coordinates saved"],
|
||
[/^正在保存所选坐标\.\.\.$/, "Saving selected coordinates..."],
|
||
[/^正在采集坐标候选\.\.\.$/, "Collecting coordinate candidates..."],
|
||
[/^采集失败:(.+)$/, "Collection failed: $1"],
|
||
[/^保存失败:(.+)$/, "Save failed: $1"],
|
||
[/^未能采集到坐标:(.+)$/, "Could not collect coordinates: $1"],
|
||
[/^未找到可采用候选:(.+)$/, "No applicable candidate found: $1"],
|
||
[/^未找到包含有效经纬度的候选$/, "No candidate includes valid coordinates"],
|
||
[/^常规来源没有可用坐标候选$/, "No coordinate candidates from regular sources"],
|
||
[/^常规来源无结果;LLM 兜底未生成可用候选:(.+)$/, "Regular sources returned no result; LLM fallback produced no usable candidate: $1"],
|
||
[/^常规来源无结果;LLM 兜底已尝试但没有返回可用候选。(.+)$/, "Regular sources returned no result; LLM fallback was attempted but returned no usable candidate. $1"],
|
||
[/^(\d+)个观测站 \((.+)\)$/, "$1 collectors ($2)"],
|
||
[/^(.+) 等(\d+)地$/, "$1 + $2 regions"],
|
||
[/^(.+) 等(\d+)处$/, "$1 + $2 sites"],
|
||
[/^近24h (\d+)条事件 \/ (\d+)个前缀$/, "24h $1 events / $2 prefixes"],
|
||
[/^一键定位进行中 (\d+)\/(\d+),已保存 (\d+) 个,失败 (\d+) 个$/, "Batch location in progress $1/$2, saved $3, failed $4"],
|
||
[/^一键定位进行中 (\d+)\/(\d+),已保存 (\d+) 个$/, "Batch location in progress $1/$2, saved $3"],
|
||
[/^已定位并采用 (\d+) 个最高置信候选,(\d+) 个仍需手动处理$/, "Applied $1 highest-confidence candidates; $2 still need manual review"],
|
||
[/^已定位并采用 (\d+) 个最高置信候选$/, "Applied $1 highest-confidence candidates"],
|
||
[/^(\d+) 个都没有可自动采用的候选,需要手动处理$/, "$1 items need manual handling; no automatic candidates were usable"],
|
||
[/^一键定位失败:(.+)$/, "Batch location failed: $1"],
|
||
[/^一键采用失败:(.+)$/, "Batch apply failed: $1"],
|
||
[/^未找到可采用候选$/, "No applicable candidate found"],
|
||
];
|
||
|
||
const SKIP_SELECTOR = [
|
||
"script",
|
||
"style",
|
||
"svg",
|
||
"canvas",
|
||
"video",
|
||
"iframe",
|
||
".material-symbols-rounded",
|
||
].join(",");
|
||
|
||
const TRANSLATABLE_ATTRIBUTES = ["title", "aria-label", "placeholder", "alt"];
|
||
|
||
let activeLocale = readStoredLocale();
|
||
let observer = null;
|
||
let translating = false;
|
||
|
||
function normalizeText(value) {
|
||
return String(value ?? "").replace(/\s+/g, " ").trim();
|
||
}
|
||
|
||
export function hasCjkText(value) {
|
||
return /[\u4e00-\u9fff]/.test(String(value ?? ""));
|
||
}
|
||
|
||
export function normalizeLocale(value) {
|
||
const normalized = String(value || "").toLowerCase();
|
||
if (normalized === "en" || normalized === "en-us" || normalized.startsWith("en-")) return "en-US";
|
||
if (normalized === "zh" || normalized === "zh-cn" || normalized.startsWith("zh-")) return "zh-CN";
|
||
return DEFAULT_LOCALE;
|
||
}
|
||
|
||
export function readStoredLocale() {
|
||
try {
|
||
const storedLocale = window.localStorage?.getItem(LOCALE_STORAGE_KEY);
|
||
if (storedLocale) return normalizeLocale(storedLocale);
|
||
const docsLang = window.localStorage?.getItem(LEGACY_DOCS_LANG_STORAGE_KEY);
|
||
if (docsLang === "en") return "en-US";
|
||
if (docsLang === "zh") return "zh-CN";
|
||
} catch {
|
||
// Ignore storage failures and fall back to the default language.
|
||
}
|
||
return DEFAULT_LOCALE;
|
||
}
|
||
|
||
export function getEarthLocale() {
|
||
return activeLocale;
|
||
}
|
||
|
||
export function docsLangFromLocale(locale) {
|
||
return normalizeLocale(locale) === "en-US" ? "en" : "zh";
|
||
}
|
||
|
||
export function getEarthLocaleLabel(locale = activeLocale) {
|
||
return LOCALE_LABELS[normalizeLocale(locale)] || LOCALE_LABELS[DEFAULT_LOCALE];
|
||
}
|
||
|
||
function syncLocaleSegmentedSliders() {
|
||
document.querySelectorAll(".earth-settings-segmented, .earth-mobile-settings-segmented").forEach((segmented) => {
|
||
if (!(segmented instanceof HTMLElement)) return;
|
||
const buttons = Array.from(segmented.querySelectorAll("[data-earth-locale]"));
|
||
if (buttons.length === 0) return;
|
||
const activeIndex = Math.max(0, buttons.findIndex((button) => button.classList.contains("is-active")));
|
||
segmented.style.setProperty("--item-count", String(buttons.length));
|
||
segmented.style.setProperty("--active-index", String(activeIndex));
|
||
});
|
||
}
|
||
|
||
export function translateText(value, locale = activeLocale) {
|
||
const text = normalizeText(value);
|
||
if (!text || normalizeLocale(locale) === "zh-CN") return text;
|
||
if (EXACT_TRANSLATIONS[text]) return EXACT_TRANSLATIONS[text];
|
||
for (const [pattern, replacement] of PATTERN_TRANSLATIONS) {
|
||
const match = text.match(pattern);
|
||
if (!match) continue;
|
||
if (typeof replacement === "function") return replacement(match, locale);
|
||
return text.replace(pattern, replacement);
|
||
}
|
||
return text;
|
||
}
|
||
|
||
export function earthMessage(key, params = {}) {
|
||
return {
|
||
[EARTH_MESSAGE_MARKER]: true,
|
||
key,
|
||
params,
|
||
};
|
||
}
|
||
|
||
export function isEarthMessage(value) {
|
||
return Boolean(value && typeof value === "object" && value[EARTH_MESSAGE_MARKER] === true);
|
||
}
|
||
|
||
function interpolateEarthMessage(template, params = {}, locale = activeLocale) {
|
||
return String(template).replace(/\{([A-Za-z0-9_]+)\}/g, (_, key) => {
|
||
const value = params[key];
|
||
return value === undefined || value === null ? "" : String(value);
|
||
});
|
||
}
|
||
|
||
export function formatEarthMessage(value, locale = activeLocale) {
|
||
if (!isEarthMessage(value)) {
|
||
return translateText(value, locale);
|
||
}
|
||
|
||
const normalizedLocale = normalizeLocale(locale);
|
||
const localeKey = normalizedLocale === "en-US" ? "en" : "zh";
|
||
const template = EARTH_MESSAGE_TEMPLATES[value.key];
|
||
if (!template) return translateText(value.key, normalizedLocale);
|
||
const params = value.params || {};
|
||
const renderer = template[localeKey] || template.zh || template.en;
|
||
if (typeof renderer === "function") {
|
||
return normalizeText(renderer(params, normalizedLocale));
|
||
}
|
||
return normalizeText(interpolateEarthMessage(renderer, params, normalizedLocale));
|
||
}
|
||
|
||
export function localizeCountryName(value, locale = activeLocale) {
|
||
const normalizedLocale = normalizeLocale(locale);
|
||
if (!value) return "";
|
||
if (typeof value === "object") {
|
||
const source = value || {};
|
||
if (normalizedLocale === "en-US") {
|
||
return normalizeText(
|
||
source.nameEn ||
|
||
source.name ||
|
||
source.NAME_EN ||
|
||
source.ADMIN ||
|
||
source.nameZh ||
|
||
source.NAME_ZH ||
|
||
"",
|
||
);
|
||
}
|
||
return normalizeText(
|
||
source.nameZh ||
|
||
source.NAME_ZH ||
|
||
source.name ||
|
||
source.nameEn ||
|
||
source.NAME_EN ||
|
||
source.ADMIN ||
|
||
"",
|
||
);
|
||
}
|
||
return translateText(value, locale);
|
||
}
|
||
|
||
export function formatLocaleDateTime(value, options = {}) {
|
||
const date = value instanceof Date ? value : new Date(value);
|
||
if (Number.isNaN(date.getTime())) return "";
|
||
return date.toLocaleString(activeLocale, { hour12: false, ...options });
|
||
}
|
||
|
||
function rememberOriginalText(node) {
|
||
if (!node.parentElement) return "";
|
||
const originals = node.parentElement.__planetEarthI18nOriginalText || new WeakMap();
|
||
if (!node.parentElement.__planetEarthI18nOriginalText) {
|
||
node.parentElement.__planetEarthI18nOriginalText = originals;
|
||
}
|
||
const current = normalizeText(node.nodeValue);
|
||
if (!originals.has(node)) {
|
||
originals.set(node, current);
|
||
return current;
|
||
}
|
||
const original = originals.get(node);
|
||
const translated = translateText(original, activeLocale);
|
||
const currentLooksSource = hasCjkText(current);
|
||
if (current !== translated && current !== original && currentLooksSource) {
|
||
originals.set(node, current);
|
||
return current;
|
||
}
|
||
return original;
|
||
}
|
||
|
||
function translateTextNode(node) {
|
||
if (!node.parentElement || node.parentElement.closest(SKIP_SELECTOR)) return;
|
||
const original = rememberOriginalText(node);
|
||
if (!original) return;
|
||
const translated = translateText(original, activeLocale);
|
||
if (node.nodeValue !== translated) {
|
||
node.nodeValue = translated;
|
||
}
|
||
}
|
||
|
||
function rememberOriginalAttribute(element, attribute) {
|
||
const dataKey = `i18nOriginal${attribute
|
||
.replace(/(^|-)([a-z])/g, (_, __, letter) => letter.toUpperCase())}`;
|
||
const current = normalizeText(element.getAttribute(attribute));
|
||
if (!element.dataset[dataKey]) {
|
||
element.dataset[dataKey] = current;
|
||
return current;
|
||
}
|
||
const original = element.dataset[dataKey];
|
||
const translated = translateText(original, activeLocale);
|
||
const currentLooksSource = hasCjkText(current);
|
||
if (current && current !== translated && current !== original && currentLooksSource) {
|
||
element.dataset[dataKey] = current;
|
||
return current;
|
||
}
|
||
return original;
|
||
}
|
||
|
||
function translateAttributes(element) {
|
||
TRANSLATABLE_ATTRIBUTES.forEach((attribute) => {
|
||
if (!element.hasAttribute(attribute)) return;
|
||
const original = rememberOriginalAttribute(element, attribute);
|
||
if (!original) return;
|
||
const translated = translateText(original, activeLocale);
|
||
if (element.getAttribute(attribute) !== translated) {
|
||
element.setAttribute(attribute, translated);
|
||
}
|
||
});
|
||
}
|
||
|
||
function translateElementTree(root) {
|
||
if (!(root instanceof Element)) return;
|
||
if (root.closest(SKIP_SELECTOR)) return;
|
||
translateAttributes(root);
|
||
root.querySelectorAll("*").forEach((element) => {
|
||
if (element instanceof Element && !element.closest(SKIP_SELECTOR)) {
|
||
translateAttributes(element);
|
||
}
|
||
});
|
||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
||
acceptNode(node) {
|
||
if (!normalizeText(node.nodeValue)) return NodeFilter.FILTER_REJECT;
|
||
if (node.parentElement?.closest(SKIP_SELECTOR)) return NodeFilter.FILTER_REJECT;
|
||
return NodeFilter.FILTER_ACCEPT;
|
||
},
|
||
});
|
||
const nodes = [];
|
||
while (walker.nextNode()) nodes.push(walker.currentNode);
|
||
nodes.forEach(translateTextNode);
|
||
}
|
||
|
||
function syncDocumentLocale() {
|
||
document.documentElement.lang = activeLocale;
|
||
document.title = translateText("智能星球计划 - 现实层宇宙全息感知", activeLocale);
|
||
}
|
||
|
||
function persistLocale(locale) {
|
||
try {
|
||
window.localStorage?.setItem(LOCALE_STORAGE_KEY, locale);
|
||
window.localStorage?.setItem(LEGACY_DOCS_LANG_STORAGE_KEY, docsLangFromLocale(locale));
|
||
} catch {
|
||
// Ignore storage failures; the current page still changes language.
|
||
}
|
||
}
|
||
|
||
export function applyEarthI18n(root = document.body) {
|
||
if (!root) return;
|
||
if (translating) return;
|
||
translating = true;
|
||
try {
|
||
syncDocumentLocale();
|
||
if (root === document || root === document.body || root instanceof Document) {
|
||
translateElementTree(document.body);
|
||
} else if (root instanceof Element) {
|
||
translateElementTree(root);
|
||
}
|
||
syncEarthLocaleControls();
|
||
} finally {
|
||
translating = false;
|
||
}
|
||
}
|
||
|
||
function observeMutations() {
|
||
if (observer || !document.body) return;
|
||
observer = new MutationObserver((mutations) => {
|
||
if (translating) return;
|
||
const roots = new Set();
|
||
let attributeChanged = false;
|
||
mutations.forEach((mutation) => {
|
||
if (mutation.type === "childList") {
|
||
mutation.addedNodes.forEach((node) => {
|
||
if (node instanceof Element) roots.add(node);
|
||
else if (node.parentElement) roots.add(node.parentElement);
|
||
});
|
||
} else if (mutation.type === "characterData" && mutation.target.parentElement) {
|
||
roots.add(mutation.target.parentElement);
|
||
} else if (mutation.type === "attributes" && mutation.target instanceof Element) {
|
||
roots.add(mutation.target);
|
||
attributeChanged = true;
|
||
}
|
||
});
|
||
if (roots.size === 0 && !attributeChanged) return;
|
||
roots.forEach((root) => applyEarthI18n(root));
|
||
});
|
||
observer.observe(document.body, {
|
||
attributes: true,
|
||
attributeFilter: TRANSLATABLE_ATTRIBUTES,
|
||
childList: true,
|
||
characterData: true,
|
||
subtree: true,
|
||
});
|
||
}
|
||
|
||
export function setEarthLocale(nextLocale, { persist = true, announce = true } = {}) {
|
||
const normalized = normalizeLocale(nextLocale);
|
||
if (normalized === activeLocale) {
|
||
applyEarthI18n();
|
||
return activeLocale;
|
||
}
|
||
activeLocale = normalized;
|
||
if (persist) persistLocale(normalized);
|
||
applyEarthI18n();
|
||
window.dispatchEvent(new CustomEvent(LOCALE_CHANGE_EVENT, { detail: { locale: activeLocale } }));
|
||
if (announce) {
|
||
window.dispatchEvent(
|
||
new CustomEvent("earth:status", {
|
||
detail: { message: earthMessage("status.languageSwitched") },
|
||
}),
|
||
);
|
||
}
|
||
return activeLocale;
|
||
}
|
||
|
||
export function syncEarthLocaleControls() {
|
||
document.querySelectorAll("[data-earth-locale]").forEach((button) => {
|
||
if (!(button instanceof HTMLButtonElement)) return;
|
||
const locale = normalizeLocale(button.dataset.earthLocale);
|
||
const active = locale === activeLocale;
|
||
button.classList.toggle("is-active", active);
|
||
button.setAttribute("aria-pressed", active ? "true" : "false");
|
||
});
|
||
syncLocaleSegmentedSliders();
|
||
document.querySelectorAll("[data-earth-locale-switch]").forEach((input) => {
|
||
if (!(input instanceof HTMLInputElement)) return;
|
||
const checked = activeLocale === "en-US";
|
||
input.checked = checked;
|
||
input.setAttribute("aria-checked", checked ? "true" : "false");
|
||
input.closest(".earth-settings-switch, .earth-mobile-settings-switch")
|
||
?.classList.toggle("is-checked", checked);
|
||
});
|
||
}
|
||
|
||
export function setupEarthLocaleControls(root = document) {
|
||
root.querySelectorAll("[data-earth-locale]").forEach((button) => {
|
||
if (!(button instanceof HTMLButtonElement) || button.dataset.localeBound === "true") return;
|
||
button.dataset.localeBound = "true";
|
||
button.addEventListener("click", () => {
|
||
setEarthLocale(button.dataset.earthLocale || DEFAULT_LOCALE);
|
||
});
|
||
});
|
||
root.querySelectorAll("[data-earth-locale-switch]").forEach((input) => {
|
||
if (!(input instanceof HTMLInputElement) || input.dataset.localeBound === "true") return;
|
||
input.dataset.localeBound = "true";
|
||
input.addEventListener("change", () => {
|
||
setEarthLocale(input.checked ? "en-US" : "zh-CN");
|
||
});
|
||
});
|
||
syncEarthLocaleControls();
|
||
}
|
||
|
||
export function onEarthLocaleChange(callback) {
|
||
const handler = (event) => callback(event.detail?.locale || activeLocale);
|
||
window.addEventListener(LOCALE_CHANGE_EVENT, handler);
|
||
return () => window.removeEventListener(LOCALE_CHANGE_EVENT, handler);
|
||
}
|
||
|
||
export function initEarthI18n() {
|
||
activeLocale = readStoredLocale();
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
applyEarthI18n();
|
||
setupEarthLocaleControls();
|
||
observeMutations();
|
||
}, { once: true });
|
||
} else {
|
||
applyEarthI18n();
|
||
setupEarthLocaleControls();
|
||
observeMutations();
|
||
}
|
||
window.addEventListener("storage", (event) => {
|
||
if (event.key !== LOCALE_STORAGE_KEY && event.key !== LEGACY_DOCS_LANG_STORAGE_KEY) return;
|
||
setEarthLocale(readStoredLocale(), { persist: false, announce: false });
|
||
});
|
||
}
|