Compare commits

..
Author SHA1 Message Date
atsunatsu 8d9208b063 build: release version 4.4.8-ba7opf.11.8 (583) 2026-10-10 01:53:03 +08:00
atsunatsu a0ebe14d07 fix(i18n): 繁体中文回退到简中(values-b+zh+Hant + localeFilters 补繁体地区)
Android 7+ 按脚本解析中文:无限定符的 values-zh 视为 zh-Hans,zh-TW/zh-HK/zh-MO(zh-Hant)
设备匹配不到它、掉到默认英文。新增 values-b+zh+Hant(values-zh 逐字副本,非新译文),并把
zh-rTW/zh-rHK/zh-rMO 补进 localeFilters——否则 aapt2 会把该配置从 APK 里裁掉,修复静默失效
(这也解释了 AndroidX 自带繁体资源为何一直不出现)。aapt2 只接受 zh-rTW 写法,zh-Hant 会
构建报错。加 TraditionalChineseFallbackTest 钉死两份文案一致与 filter 内容。
2026-10-10 01:52:59 +08:00
atsunatsu e162de7c19 build: release version 4.4.8-ba7opf.11.7 (582) 2026-10-09 03:05:03 +08:00
atsunatsu 63e2ea88b7 feat(satellites): AMSAT Live FM 组补 IO-86 / LAPAN-A2
Sources.amSatFmCatnums 加 IO-86 / LAPAN-A2 (40931):SatNOGS 有 V/U FM 话音转发器
"V/U FM Voice (PL 88.5Hz)" 145.880 / 435.880;R4UAB 名 "LAPAN-A2 (IO-86)"、
SatNOGS 3LE 名 "LAPAN-A2"、AMSAT 名 "IO-86"。LoTW 别名表早就有 LAPAN-A2 → IO-86
(ARRL 官方名),所以这颗星只是筛选里看不到,上传链路本来通。

同一集合共用三处:卫星页与互通页的 FM 筛选、雷达页 Log 的 useRepeater 判定
(voiceRepeater() 排序下唯一含 "voice" 的 FM 双工行命中,记账固定取
145.880 / 435.880 名义对;APRS/SSDV 两行是 AFSK、Mode X 无上行,都进不了候选)。

测试:SelectionRepoSearchTest 的 FM 虚拟类型用例扩到三颗(67291 / 98205 / 40931)。
2026-10-09 03:04:59 +08:00
atsunatsu ddca54e7e6 build: release version 4.4.8-ba7opf.11.6 (581) 2026-10-09 01:54:56 +08:00
atsunatsu e19d22714b feat(satellites): AMSAT Live FM 组补 RS95S 与 UNNE-1B
Sources.amSatFmCatnums 加两颗:RS95S / QMR-KWT-2 (67291)、UNNE-1B (98205)。
两星在 SatNOGS 都有 V/U FM 转发器("V/U FM Transponder CTCSS 67.0 Hz"
145.920/436.950,与 "Mode V/U - FM Repeater (Voice + (A)FSK)"),因此归入
卫星页「AMSAT Live FM」虚拟类型筛选。该集合是三处共用的一份口径:卫星页与
互通页的 FM 筛选、以及雷达页 Log(FM 星按名义 voice repeater 频率对记账,
两颗星都有该行,不会回落到算器/所选频点)。

98205 是 SatNOGS 的分析编号(国际编号 26227BL,T-18 由 OTV OSSIE 释放),
Space-Track 正式编目后会换号,届时需回改这一条;已在 KDoc 标注,ephem-watch
cron(6b1544af4470)会盯到编号变化。

测试:SelectionRepoSearchTest 新增一例,断言 FM 虚拟类型命中 67291 与 98205
(全量 ./gradlew test 通过)。
2026-10-09 01:47:52 +08:00
atsunatsu 623d859c97 test(mutual): 钉死 fixture 时间基准,消除时段性 flaky
fixture 用 2024-03-09 固定 ISS TLE + System.currentTimeMillis() 起点;SGP4 带阻力衰减 942 天后高度仅 ≈262km,12h 窗口内可能无广州过境 → 3 条依赖过境窗的用例时段性失败(Gradle 缓存 FROM-CACHE 会掩盖上一次的真跑)。

- TestOrbits.FIXTURE_BASE(2024-03-10T00:00Z,历元后约 18h)取代默认 System.currentTimeMillis()

- MutualViewModel 墙钟可注入:nowMillis: () -> Long = System::currentTimeMillis(第 4 个默认参数,生产行为逐位不变;工厂具名构造零改动)

- 断言里的 now 改用同一基准;prefill/scroll 测试不碰时间未动

修后::feature:mutual 28/28,全量 ./gradlew test 451/451。
2026-10-08 12:36:40 +08:00
atsunatsu 08803e0f55 feat(logbook): LoTW 卫星别名补 RS95S(QMR-KWT 2)+内置 config.tq6 升 11.35
RS-95S(QMR-KWT 2,NORAD 67291)QSO 上传时发现:别名表无该星、内置 config 11.34 也无此官方名(11.34 里没有 RS95S,会被 every-alias-target 校验挡住)。

- config.tq6 11.34 → 11.35(ARRL 现行版本,新增 RS95S=QMR-KWT-2)

- 别名表补 QMRKWT2→RS95S(SatNOGS 名 QMR-KWT 2)与 RS95S→RS95S(兼容 RS-95S 连字符写法,resolve 的包含回退逐字符比会漏)

- 单测补断言:QMR-KWT 2 lookup / RS-95S+QMR-KWT 2 resolve / 用真实 config 解析 RS95S
2026-10-08 12:36:35 +08:00
14 changed files with 698 additions and 15 deletions

No files matched your search

@@ -74,7 +74,13 @@ internal fun Project.setupAndroidApp() {
}
androidResources {
generateLocaleConfig = true
localeFilters.addAll(listOf("en", "es", "ru", "si", "tr", "uk", "zh"))
// "zh" alone is stripped down to the Hans config: without the Traditional
// region entries below, values-b+zh+Hant is filtered out of the APK and a
// zh-TW / zh-HK / zh-MO device falls back to English. aapt2 only accepts
// the legacy "zh-rTW" spelling here ("zh-Hant" fails the build).
localeFilters.addAll(
listOf("en", "es", "ru", "si", "tr", "uk", "zh", "zh-rTW", "zh-rHK", "zh-rMO")
)
}
packaging { resources { excludes += listOf("META-INF/*") } }
}
Binary file not shown.
@@ -36,7 +36,7 @@ import java.util.Locale
import java.util.zip.GZIPInputStream
import javax.xml.parsers.DocumentBuilderFactory
/** Bundled ARRL configuration 11.34 from https://lotw.arrl.org/lotw/config.tq6. */
/** Bundled ARRL configuration 11.35 from https://lotw.arrl.org/lotw/config.tq6. */
internal class LoTWConfig(input: InputStream) {
private val root = input.use { source -> GZIPInputStream(source).use { gzip ->
val xml = gzip.bufferedReader(Charsets.UTF_8).readText()
@@ -87,6 +87,7 @@ class LoTWConfigTest {
assertEquals("AO-91", config.resolveSatellite("FOX-1B"))
assertEquals("IO-86", config.resolveSatellite("LAPAN-A2"))
assertEquals("RS-44", config.resolveSatellite("DOSAAF-85"))
assertEquals("RS95S", config.resolveSatellite("QMR-KWT 2"))
assertEquals("BO-102", config.resolveSatellite("CAS-7B"))
assertEquals("SO-50", config.resolveSatellite("SO-50"))
}
@@ -124,6 +124,24 @@ class SelectionRepoSearchTest {
assertEquals(setOf(25544, 61781), results.map { it.catnum }.toSet())
}
@Test
fun `FM virtual type includes the BA7OPF RS95S UNNE-1B and IO-86 additions`() = runTest {
// RS95S / QMR-KWT-2 (67291), UNNE-1B (98205) and IO-86 / LAPAN-A2 (40931)
// joined the hardcoded AMSAT Live FM set on 2026-10-09; each carries a V/U
// FM repeater in SatNOGS ("V/U FM Transponder CTCSS 67.0 Hz",
// "Mode V/U - FM Repeater (Voice + (A)FSK)", "V/U FM Voice (PL 88.5Hz)").
// sampleItems contributes ISS (25544) only.
val items = sampleItems + listOf(
SatItem(catnum = 67291, name = "QMR-KWT-2 (RS95S)"),
SatItem(catnum = 98205, name = "UNNE-1B"),
SatItem(catnum = 40931, name = "LAPAN-A2 (IO-86)")
)
val repo = createRepo(items)
repo.setTypes(listOf("AMSAT Live FM"))
val results = repo.getEntriesFlow().first()
assertEquals(setOf(25544, 67291, 98205, 40931), results.map { it.catnum }.toSet())
}
@Test
fun `Linear virtual type shows only hardcoded linear satellites`() = runTest {
val repo = createRepo(sampleItems)
@@ -34,6 +34,8 @@ import java.util.Locale
* - live SatNOGS 3LE names and Celestrak amateur names joined to the same NORAD id,
* - designator tokens of the config.tq6 satellite descriptions (e.g. CAS-7B -> BO-102),
* - the ISS family, which SatNOGS does not list under its ARRL name "ARISS".
* Added 2026-10-08: config.tq6 v11.35 lists QMR-KWT 2 as "RS95S"; SatNOGS DB carries it as
* "QMR-KWT 2" (alias "RS95S") and other sources spell it "RS-95S", hence the identity entry.
* Placeholder names ("OBJECT AY" and friends) are deliberately NOT mapped: they are recycled
* between objects and would sign the wrong satellite. LoTWConfigTest asserts that every target
* here is a real config.tq6 name.
@@ -79,11 +81,11 @@ object LoTWSatelliteAliases {
"OSCAR16PACSAT" to "AO-16", "OSCAR19LUSAT" to "LO-19", "OSCAR64" to "DO-64",
"OSCAR7" to "AO-7", "PACSAT" to "AO-16", "PARKINSONSAT" to "NO-84",
"PCSAT" to "NO-44", "PHILLIPINESOSCAR101" to "PO-101", "PO101" to "PO-101",
"PSAT" to "NO-84", "PSAT2" to "NO-104", "QB50P1" to "EO-79",
"PSAT" to "NO-84", "PSAT2" to "NO-104", "QB50P1" to "EO-79", "QMRKWT2" to "RS95S",
"QO100" to "QO-100", "RADFXSAT" to "AO-91", "RADIOROSTO" to "RS-15",
"RS0ISS" to "ARISS", "RS14" to "AO-21", "RS15" to "RS-15",
"RS44" to "RS-44", "RS64S" to "AO-123", "SAUDISAT1C" to "SO-50",
"SO121" to "SO-121", "SO124" to "SO-124", "SO125" to "SO-125",
"RS44" to "RS-44", "RS64S" to "AO-123", "RS95S" to "RS95S",
"SAUDISAT1C" to "SO-50", "SO121" to "SO-121", "SO124" to "SO-124", "SO125" to "SO-125",
"SO50" to "SO-50", "SOLUTUSNANOSATTELITE" to "SONATE", "SONATE2" to "SONATE",
"TAURUS1" to "TAURUS", "TEVEL21" to "TEV2-1", "TEVEL22" to "TEV2-2",
"TEVEL23" to "TEV2-3", "TEVEL24" to "TEV2-4", "TEVEL25" to "TEV2-5",
@@ -50,11 +50,30 @@ object Sources {
)
/**
* Hardcoded AMSAT Live FM satellites (NORAD catnums):
* SO-50 (27607), ISS ZARYA (25544), AO-123 ASRTU-1 (61781).
* SO-50 (27607), ISS ZARYA (25544), AO-123 ASRTU-1 (61781),
* RS95S/QMR-KWT-2 (67291), UNNE-1B (98205).
* Replaces the AMSAT live-page fetch: stable on any network, no
* sync-time dependency, no stale-list or timeout failure modes.
*
* BA7OPF additions (2026-10-09):
* - RS95S / QMR-KWT-2 (67291): V/U FM transponder 145.920/436.950 CTCSS 67.0
* (SatNOGS "QMR-KWT-2 (RS95S)", R4UAB "QMR-KWT-2 (RS95S)", AMSAT "RS95S
* (QMR-KWT-2)"; the LoTW alias table maps the tracker name to ARRL's "RS95S").
* - UNNE-1B (98205): AMSAT-EA 1.5P PocketQube released from the T-18 OTV
* "OSSIE" with a V/U FM repeater ("Voice + (A)FSK").
* - IO-86 / LAPAN-A2 (40931): V/U FM voice repeater 145.880/435.880 PL 88.5
* (SatNOGS "V/U FM Voice (PL 88.5Hz)"; R4UAB "LAPAN-A2 (IO-86)", SatNOGS
* 3LE "LAPAN-A2", AMSAT "IO-86"; the LoTW alias table maps LAPAN-A2 to
* ARRL's "IO-86"). This fork's first TLE source keeps its elements fresh.
* ⚠ 98205 is SatNOGS' analysis number (designator 26227BL) for the freshly
* released object — Space-Track assigns the permanent catalogue number later,
* so this entry must be re-pointed then; the ephem-watch cron job tracks it.
*
* The set is shared: the satellite page / mutual page "AMSAT Live FM" filters
* and the radar Log page (FM satellites are logged on the fixed nominal
* voice-repeater pair).
*/
val amSatFmCatnums = setOf(27607, 25544, 61781)
val amSatFmCatnums = setOf(27607, 25544, 61781, 67291, 98205, 40931)
/**
* Hardcoded AMSAT Live linear (SSB/CW) satellites:
@@ -27,7 +27,7 @@ import org.junit.Test
/** Alias table that maps tracker catalogue names onto the names ARRL's config.tq6 uses. */
class LoTWSatelliteAliasesTest {
private val catalogue = listOf("SO-50", "ARISS", "PO-101", "BO-102", "AO-91", "RS-44", "IO-117")
private val catalogue = listOf("SO-50", "ARISS", "PO-101", "BO-102", "AO-91", "RS-44", "IO-117", "RS95S")
@Test
fun `lookup maps SatNOGS catalogue names to ARRL names`() {
@@ -37,6 +37,7 @@ class LoTWSatelliteAliasesTest {
assertEquals("AO-91", LoTWSatelliteAliases.lookup("FOX-1B"))
assertEquals("RS-44", LoTWSatelliteAliases.lookup("DOSAAF-85"))
assertEquals("IO-117", LoTWSatelliteAliases.lookup("GREENCUBE"))
assertEquals("RS95S", LoTWSatelliteAliases.lookup("QMR-KWT 2"))
}
@Test
@@ -59,6 +60,8 @@ class LoTWSatelliteAliasesTest {
assertEquals("SO-50", LoTWSatelliteAliases.resolve("SAUDISAT 1C (SO-50)", catalogue))
assertEquals("ARISS", LoTWSatelliteAliases.resolve("ISS (ZARYA)", catalogue))
assertEquals("AO-91", LoTWSatelliteAliases.resolve("AO-91 (RadFxSat)", catalogue))
assertEquals("RS95S", LoTWSatelliteAliases.resolve("RS-95S", catalogue))
assertEquals("RS95S", LoTWSatelliteAliases.resolve("QMR-KWT 2", catalogue))
}
@Test
@@ -0,0 +1,529 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Traditional-Chinese (zh-Hant) devices fall back to the Simplified Chinese
strings in values-zh/ instead of the English default. Android 7+ resolves
Chinese by script, so an unqualified values-zh is treated as zh-Hans and is
never matched by a zh-TW / zh-HK / zh-MO (Hant) device.
This file is a verbatim copy of values-zh/strings.xml — do NOT translate it
here. Edit values-zh/strings.xml and copy the change over; the unit test
TraditionalChineseFallbackTest fails if the two files ever diverge.
-->
<!-- Commmon -->
<string name="btn_accept">确定</string>
<string name="btn_cancel">取消</string>
<string name="empty_list_message">没有数据可显示</string>
<!-- Navigation -->
<string name="nav_sat">卫星</string>
<string name="nav_pass">过境</string>
<string name="nav_radar">雷达</string>
<string name="nav_mutual">匹配</string>
<string name="nav_map">地图</string>
<string name="nav_prefs">设置</string>
<!-- Satellites screen -->
<string name="sat_type_hint">类型:%s</string>
<string name="sat_type_title">选择卫星类型</string>
<string name="sat_search_hint">Id - 名称</string>
<string name="sat_search_clear">清空</string>
<string name="sat_clear_all">全部清空</string>
<string name="sat_select_all">全选</string>
<string name="sat_group_selected">已选择</string>
<string name="sat_group_available">可用</string>
<string name="sat_group_selected_count">已选择(%1$d)</string>
<string name="sat_group_available_count">可用(%1$d)</string>
<string name="sat_empty_list_message">请确保您的搜索查询正确且数据库已更新</string>
<string name="sat_warning_title">警告\!</string>
<string name="sat_warning_message">此应用收录了超过9000颗卫星信息\n同时追踪所有卫星并不现实\n建议始终通过搜索和分类筛选功能仅勾选您感兴趣的卫星</string>
<!-- Passes screen -->
<string name="pass_filter_title">筛选过境</string>
<string name="pass_filter_elev">仰角值</string>
<string name="pass_filter_hours">提前时间</string>
<string name="pass_filter_history_hours">历史过境时间</string>
<string name="pass_filter_aos_time">AOS 时间段</string>
<string name="pass_filter_invert_time">反选 AOS 时间段</string>
<string name="pass_filter_deep_space">DeepSpace(周期 &gt;225分钟)</string>
<string name="pass_modes_title">选择调制类型</string>
<string name="pass_elevation">仰角:%.1f°</string>
<string name="pass_altitude">高度:%d km</string>
<string name="pass_empty_list_message">请确保您的过滤器设置正确,并且已选择卫星</string>
<string name="pass_error_message">没有即将过境的卫星</string>
<string name="pass_welcome_title">欢迎来到 Look4Sat\!</string>
<string name="pass_welcome_message">请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性</string>
<!-- AMSAT Status screen -->
<string name="nav_amsat">AMSAT</string>
<string name="amsat_title">AMSAT 卫星状态</string>
<string name="amsat_refresh">刷新</string>
<string name="amsat_retry">重试</string>
<string name="amsat_updated">更新于:</string>
<string name="amsat_active">活跃</string>
<string name="amsat_tlm">遥测/信标</string>
<string name="amsat_not_heard">未听到</string>
<string name="amsat_conflict">冲突</string>
<string name="amsat_name">名称</string>
<string name="amsat_load_failed">加载失败 - 检查网络后重试</string>
<string name="amsat_no_reports">此时间段无报告</string>
<string name="amsat_close">关闭</string>
<string name="amsat_upload">上传</string>
<string name="amsat_upload_back">返回</string>
<string name="amsat_upload_report">上传报告到 AMSAT</string>
<string name="amsat_upload_status">状态</string>
<string name="amsat_upload_callsign">呼号</string>
<string name="amsat_upload_grid">网格</string>
<string name="amsat_upload_time_now">时间:当前 UTC %s</string>
<string name="amsat_upload_submit">提交到 AMSAT</string>
<string name="amsat_upload_confirm">确认上传</string>
<string name="amsat_upload_confirm_title">确认上传到 AMSAT</string>
<string name="amsat_upload_confirm_message">这条公开报告将上传到 AMSAT。\n卫星:%1$s\n状态:%2$s\n呼号:%3$s\n网格:%4$s</string>
<string name="amsat_upload_grid_none">未填写</string>
<string name="amsat_upload_success">报告已上传</string>
<string name="amsat_upload_failed">上传失败:%s</string>
<string name="amsat_upload_callsign_required">请填写呼号</string>
<string name="amsat_upload_grid_invalid">网格应为 4 或 6 位 Maidenhead 定位符</string>
<string name="amsat_upload_active">活跃</string>
<string name="amsat_upload_tlm">遥测/信标</string>
<string name="amsat_upload_not_heard">未听到</string>
<string name="amsat_upload_crew_active">ISS 语音</string>
<!-- Radar screen -->
<string name="radar_back">后退</string>
<string name="radar_notify">通知</string>
<string name="radar_az_text">方位角</string>
<string name="radar_el_text">仰角</string>
<string name="radar_alt_text">高度</string>
<string name="radar_dist_text">距离</string>
<string name="radar_eclipsed">地影中</string>
<string name="radar_downlink">下行链路</string>
<string name="radar_uplink">上行链路</string>
<string name="radar_no_data">这颗卫星没有收发器</string>
<string name="radar_add">添加</string>
<string name="radar_data">卫星收发器:</string>
<string name="radar_inverted">倒置:%s</string>
<string name="radar_mode">调制:%s</string>
<string name="radar_string_no">否</string>
<string name="radar_string_yes">是</string>
<string name="radar_visible">日照中</string>
<string name="radar_doppler_calc">多普勒频率计算器</string>
<string name="radar_doppler_tx_hint">输入上行频率 (MHz)</string>
<string name="radar_doppler_rx_hint">输入下行频率 (MHz)</string>
<string name="radar_doppler_offset_hint">偏移 (kHz)</string>
<string name="radar_doppler_info">线性转发器:输入一个频率即可算出另一个</string>
<string name="radar_cw_decoder">CW 解码器</string>
<string name="radar_cw_start">开始</string>
<string name="radar_cw_stop">停止</string>
<string name="radar_cw_reset">清空</string>
<!-- Map screen -->
<string name="map_prev">上一个</string>
<string name="map_next">下一个</string>
<string name="map_grid_mode">网格模式</string>
<string name="map_satellite_mode">卫星模式</string>
<string name="map_first_call_mode">首通呼号</string>
<string name="map_azimuth">方位角:%.1f°</string>
<string name="map_elevation">仰角:%.1f°</string>
<string name="map_altitude">高度:%.0f km</string>
<string name="map_distance">距离:%.0f km</string>
<string name="map_latitude">纬度:%.1f°</string>
<string name="map_longitude">经度:%.1f°</string>
<string name="map_phase">相位:%.1f°</string>
<string name="map_eclipsed">地影中</string>
<string name="map_period">周期:% .0f min</string>
<string name="map_velocity">速度:%.2f km/s</string>
<string name="map_visibility">能见度:%s</string>
<!-- Worked-grid QSO dialog -->
<string name="grid_qso_calls_count" formatted="false">%d 个呼号 · %d 次通联</string>
<string name="region_qso_empty">该区域暂无已确认通联</string>
<string name="grid_qso_count_one">%d QSO</string>
<string name="grid_qso_count_many">%d QSOs</string>
<string name="grid_qso_first">首通</string>
<string name="grid_qso_no_sat">卫星未知</string>
<string name="grid_qso_match">匹配</string>
<!-- 未通联网格中的“标记想通联的电台”提醒 -->
<string name="grid_mark_station">标记你想通联的电台</string>
<string name="grid_mark_callsign">呼号</string>
<string name="grid_mark_confirm">标记</string>
<string name="grid_mark_label">标记</string>
<string name="grid_mark_delete">删除标记</string>
<string name="grid_mark_pin">置顶</string>
<string name="map_visible">日照中</string>
<!-- Settings screen -->
<string name="prefs_privacy_title">隐私政策</string>
<string name="prefs_updated_title">更新:%s</string>
<string name="prefs_updated_time">yyyy年M月d日 HH:mm:ss</string>
<string name="prefs_loc_title">站点位置</string>
<string name="prefs_loc_gps_title">GPS 定位</string>
<string name="prefs_loc_gps_error">检查您的位置权限</string>
<string name="prefs_loc_input_title">输入位置</string>
<string name="prefs_loc_input_error">输入的位置无效</string>
<string name="prefs_loc_qth_title">QTH 网格</string>
<string name="prefs_loc_qth_error">无效的 QTH 网格</string>
<string name="prefs_loc_success">位置更新成功</string>
<string name="prefs_station_title">站点位置设置</string>
<string name="prefs_station_lat_text">输入站点的纬度</string>
<string name="prefs_station_lon_text">输入站点的经度</string>
<string name="prefs_locator_title">QTH 网格</string>
<string name="prefs_wavelog_configure">配置</string>
<string name="prefs_wavelog_url">Wavelog 地址</string>
<string name="prefs_wavelog_syncing">同步中…</string>
<string name="prefs_wavelog_upload_title">Wavelog 配置</string>
<string name="prefs_wavelog_exclusive">与 LoTW 二选一配置,两者不兼容。</string>
<string name="prefs_wavelog_sync_incremental">增量同步</string>
<string name="prefs_wavelog_sync_full">全量同步</string>
<string name="prefs_wavelog_clear">清除配置</string>
<string name="prefs_wavelog_clear_title">清除 Wavelog 配置?</string>
<string name="prefs_wavelog_clear_message">清除后将切回 LoTW 模式,LoTW 功能恢复可用。</string>
<string name="prefs_wavelog_switch_title">配置提醒</string>
<string name="prefs_wavelog_switch_message">已配置 LoTW。配置 Wavelog 后,App 内 LoTW 的上传与同步将不可用(两者二选一,不兼容)。继续?</string>
<string name="prefs_wavelog_switch_continue">继续</string>
<string name="prefs_lotw_disabled_wavelog">Wavelog 模式中不可用(二选一)</string>
<string name="prefs_wavelog_upload_not_configured">未配置 — 把本机通联记录上传到 Wavelog</string>
<string name="prefs_wavelog_upload_configured">站点 %1$s — 可以上传</string>
<string name="prefs_wavelog_upload_apikey">API key(读写权限)</string>
<string name="prefs_wavelog_upload_fetch">获取站点</string>
<string name="prefs_wavelog_upload_probing">检测中…</string>
<string name="prefs_wavelog_upload_station_none">未选择站点 — 点「获取站点」</string>
<string name="prefs_wavelog_upload_rights_rw">密钥:读写 — 可上传</string>
<string name="prefs_wavelog_upload_rights_r">密钥只读 — 请在 Wavelog → API 生成「读写」密钥</string>
<string name="prefs_wavelog_upload_hint">点「获取站点」选择上传使用的台站档案;密钥需要「读写」权限。</string>
<string name="prefs_lotw_title">LoTW 已确认网格</string>
<string name="prefs_lotw_callsign">LoTW 呼号</string>
<string name="prefs_lotw_password">LoTW 密码</string>
<string name="prefs_lotw_hint">直接从 ARRL LoTW 拉取该呼号已确认 QSO 的网格并覆盖本地已存集合(当前 %1$d 个)。密码仅保存在手机本地。点"同步"会自动保存凭据并拉取。</string>
<string name="prefs_lotw_sync">同步</string>
<string name="prefs_lotw_syncing">同步中…</string>
<string name="prefs_lotw_configured">已从 LoTW 同步 — 共 %1$d 个网格</string>
<string name="prefs_lotw_not_configured">未配置 — 直接登录 ARRL LoTW 拉取已确认 QSO 的网格,无需自建服务器</string>
<string name="lotw_sync_error_not_configured">LoTW 呼号/密码未配置</string>
<string name="lotw_sync_error_credentials">LoTW 同步失败——呼号或密码错误</string>
<string name="lotw_sync_error_rate_limited">LoTW 同步失败——服务器限流,请等几分钟后再试</string>
<string name="lotw_sync_error_timeout">LoTW 同步失败——连接超时,请更换网络重试</string>
<string name="lotw_sync_error_network">LoTW 同步失败——网络错误(%1$s)</string>
<string name="lotw_sync_full">全量同步</string>
<string name="lotw_sync_incremental">增量合并</string>
<string name="lotw_sync_rate_hint">下载速度约每 100 条 QSO 需 8 秒(另有 5–40 秒服务器生成时间)</string>
<string name="lotw_sync_progress_connecting">正在连接 LoTW 服务器…</string>
<string name="lotw_sync_progress_downloading">正在下载报告…</string>
<string name="lotw_sync_progress_saving">正在处理下载的数据…</string>
<string name="lotw_sync_progress_count">本次同步 %1$d 条 QSO…</string>
<string name="lotw_sync_progress_qso">本次同步 %1$d 条 QSO,预计还需 %2$d 秒</string>
<string name="prefs_locator_text">使用梅登黑德网格设置站点位置</string>
<string name="btn_delete">删除</string>
<string name="prefs_logbook_title">日志本</string>
<string name="prefs_logbook_count">%1$d 条记录 — 本地通联与 LoTW 确认</string>
<string name="prefs_logbook_empty">暂无记录 — 在雷达页 Log 标签记录通联</string>
<string name="prefs_logbook_close">关闭</string>
<string name="prefs_logbook_upload">上传</string>
<string name="prefs_logbook_upload_title">上传到 LoTW</string>
<string name="prefs_logbook_upload_confirm">确认上传</string>
<string name="prefs_logbook_resubmit">重传选中 (%1$d)</string>
<string name="prefs_logbook_selected">已选 %1$d 条</string>
<string name="prefs_logbook_select_hint">长按记录行可多选重传</string>
<string name="prefs_logbook_resubmit_note">已上传记录将按当前台址就地更新;已被奖项使用的旧网格可能被拒绝。</string>
<string name="prefs_logbook_callsign_missing">其中 %1$d 条记录没有呼号,将按证书 %2$s 上传。</string>
<string name="prefs_logbook_callsign_different">%1$d 条记录登记的不是证书 %2$s 的呼号。</string>
<string name="prefs_logbook_callsign_rewrite">改写为 %1$s</string>
<string name="prefs_logbook_callsign_switch">换一张证书</string>
<string name="prefs_logbook_station_label">台址</string>
<string name="prefs_logbook_station_all">全部</string>
<string name="prefs_logbook_upload_wavelog_title">上传到 Wavelog</string>
<string name="lotw_upload_grid_title">上传前核对</string>
<string name="lotw_upload_grid_fix">修改台址</string>
<string name="lotw_upload_grid_ignore">忽略</string>
<string name="lotw_upload_grid_mismatch">当前定位 %1$s 不在证书台址网格 %2$s 内——如果正在此处操作,请先更新台址再上传。</string>
<string name="lotw_upload_pos_hint">当前定位 %1$s 不在证书台址网格 %2$s 内 · 点此更新</string>
<string name="prefs_lotw_upload_title">LoTW 上传证书</string>
<string name="prefs_lotw_upload_configured">证书已导入 · 台址网格 %1$s</string>
<string name="prefs_lotw_upload_not_configured">未配置 — 导入 TrustedQSL 证书后即可上传通联</string>
<string name="prefs_lotw_upload_busy">处理中…</string>
<string name="prefs_lotw_upload_cert_hint">导入你的 TrustedQSL .p12 证书(与 TQSL 同款)用于签名上传通联。密码仅加密保存在手机本地。</string>
<string name="prefs_lotw_upload_password">证书密码</string>
<string name="prefs_lotw_upload_password_hint">证书无密码时留空即可</string>
<string name="prefs_lotw_upload_import">选择 .p12 文件</string>
<string name="prefs_lotw_upload_import_confirm">导入</string>
<string name="prefs_lotw_upload_error_password">证书密码错误,请检查 TQSL 导出 .p12 时设置的密码</string>
<string name="prefs_lotw_upload_error_expired">证书已过期或尚未生效</string>
<string name="prefs_lotw_upload_error_invalid">不是有效的 LoTW 证书文件</string>
<string name="prefs_lotw_upload_error_format">不支持的 .p12 加密,请用 TQSL 默认加密重新导出后再试</string>
<string name="prefs_lotw_upload_error_format_alg">不支持的 .p12 加密(%1$s),请用 TQSL 默认加密重新导出后再试</string>
<string name="prefs_lotw_upload_error_unknown">导入失败,请重试</string>
<string name="prefs_lotw_upload_cert_info">%1$s · DXCC %2$d · 有效期至 %3$s</string>
<string name="prefs_lotw_upload_remove">移除证书</string>
<string name="prefs_lotw_upload_station_title">台址</string>
<string name="prefs_lotw_upload_region_hint">未选择</string>
<string name="prefs_lotw_upload_region_label">省份</string>
<string name="prefs_lotw_upload_region_pending">导入证书后可选</string>
<string name="prefs_lotw_upload_grid">网格(逗号分隔,可多格)</string>
<string name="prefs_lotw_upload_save">保存</string>
<string name="prefs_lotw_upload_close">关闭</string>
<string name="prefs_data_title">卫星数据更新</string>
<string name="prefs_data_entries">卫星:%s</string>
<string name="prefs_data_radios">收发器:%s</string>
<string name="prefs_data_update">在线更新</string>
<string name="prefs_data_import">导入文件</string>
<string name="prefs_data_clear">清空</string>
<string name="prefs_data_clear_success">数据清空成功</string>
<string name="prefs_data_update_success">更新成功</string>
<string name="prefs_data_import_satellites_error">未导入卫星。请选择有效的 TLE/3LE (.txt) 或 OMM (.csv) 文件。</string>
<string name="prefs_data_import_transceivers_error">未导入收发器。请选择有效的 SatNOGS (.json) 文件。</string>
<string name="prefs_data_sources_title">自定义数据源</string>
<string name="prefs_data_sources_sat_hint">轨道数据优先取列表最新星历,此顺序仅影响卫星名称</string>
<string name="prefs_data_sources_transceivers_hint">靠前优先:收发器数据</string>
<string name="prefs_data_sources_restore">恢复默认数据源</string>
<string name="prefs_data_sources_tle_switch">自定义TLE URL</string>
<string name="prefs_data_sources_transceivers_switch">自定义收发器URL</string>
<string name="prefs_data_sources_url_title" translatable="false">URL</string>
<string name="prefs_data_sources_satellites_label">卫星数据</string>
<string name="prefs_data_sources_transceivers_label">收发器数据</string>
<string name="prefs_data_output_title">数据输出</string>
<string name="prefs_net_output">网络</string>
<string name="prefs_bt_output">蓝牙</string>
<string name="prefs_cat_output">CAT</string>
<string name="prefs_net_title">网络数据输出</string>
<string name="prefs_net_rotator_switch">启用方位俯仰角输出</string>
<string name="prefs_net_rotator_address_hint">IP:端口</string>
<string name="prefs_net_rotator_format_hint">数据格式</string>
<string name="prefs_net_frequency_switch">启用多普勒频率输出</string>
<string name="prefs_net_frequency_address_hint">IP:端口</string>
<string name="prefs_net_frequency_format_hint">数据格式</string>
<string name="prefs_net_frequency_offset_hint" translatable="false">频率偏移 (Hz)</string>
<string name="prefs_net_frequency_offset_help" translatable="false">范围:-50000 到 50000 Hz。正值提高上报频率,负值降低上报频率。</string>
<string name="prefs_bt_title">蓝牙数据输出</string>
<string name="prefs_bt_rotator_switch">启用方位俯仰角输出</string>
<string name="prefs_bt_rotator_device_hint">蓝牙设备 ID</string>
<string name="prefs_bt_rotator_output_hint">数据格式</string>
<string name="prefs_bt_frequency_switch">启用多普勒频率输出</string>
<string name="prefs_bt_frequency_device_hint">蓝牙设备 ID</string>
<string name="prefs_bt_frequency_output_hint">数据格式</string>
<string name="prefs_bt_perm_error">请检查蓝牙权限</string>
<string name="prefs_net_perm_error">请检查网络权限</string>
<!-- Radio Control -->
<string name="nav_radiocontrol">电台控制</string>
<string name="rc_settings_title">CAT 电台控制</string>
<string name="rc_radio_model">电台型号</string>
<string name="rc_tx_device_hint">发射电台蓝牙地址</string>
<string name="rc_rx_device_hint">接收电台蓝牙地址</string>
<string name="rc_tx_name_hint">发射电台名称</string>
<string name="rc_rx_name_hint">接收电台名称</string>
<string name="rc_enable_switch">启用 CAT 控制</string>
<string name="prefs_other_title">其他设置</string>
<string name="prefs_map_title">地图设置</string>
<string name="prefs_map_configure">配置地图</string>
<string name="prefs_map_source_title">地图来源</string>
<string name="prefs_map_source_osm">离线 OSM 地图</string>
<string name="prefs_map_source_tianditu_vector">天地图(矢量)</string>
<string name="prefs_map_source_tianditu_image">天地图(影像)</string>
<string name="prefs_map_tianditu_key">天地图访问密钥</string>
<string name="prefs_map_tianditu_key_help">使用天地图前,需先在天地图开发者控制台申请服务端应用 Key。</string>
<string name="prefs_map_tianditu_key_required">选择天地图需要填写访问密钥;保存密钥前将继续使用离线 OSM 地图。</string>
<string name="prefs_other_switch_utc">以 UTC 时间显示</string>
<string name="prefs_other_switch_update">启用卫星数据自动更新</string>
<string name="prefs_other_switch_lotw_sync">自动同步 LoTW 已确认网格</string>
<string name="prefs_other_switch_sweep">启用雷达扫描动画</string>
<string name="prefs_other_switch_sensors">使用传感器旋转雷达视图</string>
<string name="prefs_compass_calibration_button">校准指南针</string>
<string name="prefs_compass_calibration_title">指南针校准</string>
<string name="prefs_compass_calibration_instruction">缓慢移动手机画“8”字,直到精度提高。请远离磁铁和金属物体。</string>
<string name="prefs_compass_accuracy">精度:%s</string>
<string name="prefs_compass_accuracy_high">高——校准完成</string>
<string name="prefs_compass_accuracy_medium">中——请继续移动</string>
<string name="prefs_compass_accuracy_low">低——请继续移动</string>
<string name="prefs_compass_accuracy_unreliable">不可靠——请开始“8”字动作</string>
<string name="prefs_compass_accuracy_unavailable">设备没有可用的磁场传感器</string>
<string name="prefs_compass_heading">校正后航向:%.1f°</string>
<string name="prefs_compass_offset">手动航向偏置(°)</string>
<string name="prefs_compass_offset_support">范围 −180° 至 +180°;正值为顺时针偏移。</string>
<string name="prefs_compass_offset_reset">偏置归零</string>
<string name="prefs_other_switch_light_theme">浅色主题</string>
<string name="prefs_other_switch_night_mode">启用红色夜间模式</string>
<string name="prefs_other_switch_map_grid">地图显示 Maidenhead 网格</string>
<string name="prefs_highlight_title">仰角高亮</string>
<string name="prefs_highlight_low">低 &lt; %1$d°</string>
<string name="prefs_highlight_mid">中 %1$d-%2$d°</string>
<string name="prefs_highlight_high">高 &gt; %1$d°</string>
<string name="prefs_outro_title">致谢与声明</string>
<string name="prefs_project_wiki">Project Wiki</string>
<string name="project_wiki_intro">Look4Sat-BA7OPF 是基于 Look4Sat 的定制版,保留上游全部功能,并新增:</string>
<string name="project_wiki_section_logbook">卫星日志本与 LoTW</string>
<string name="project_wiki_logbook_1">卫星通联日志本——通联时一键记录呼号、时间、频率、模式、信号报告和网格。</string>
<string name="project_wiki_logbook_2">LoTW 直传——导入 ARRL 证书后,直接在 App 内把日志上传到 LoTW,无需电脑。</string>
<string name="project_wiki_logbook_3">确认同步——一键同步 LoTW 官方确认,日志自动标记“已上传 / 已确认”;修改已上传的记录会自动新建一条,原记录不受影响。</string>
<string name="project_wiki_logbook_how">用法:通联后点“记录”→ 日志页点“上传”→ 设置里“同步”查看确认。</string>
<string name="project_wiki_section_mutual">双站过境匹配</string>
<string name="project_wiki_mutual_1">友台共视匹配——输入你和友台的位置,找出双方同时可见的卫星过境,点一下直接进雷达图。</string>
<string name="project_wiki_mutual_how">用法:Match 页 → 设置两站位置 → 查询。</string>
<string name="project_wiki_section_grids">通联网格地图</string>
<string name="project_wiki_grids_1">已通联网格——LoTW/Wavelog 同步的网格在地图上标绿,支持奖状边界和首通呼号标签;点格子看该网格的通联记录,还能一键去匹配友台。</string>
<string name="project_wiki_grids_how">用法:地图页开启网格模式 → 设置里同步 LoTW/Wavelog。</string>
<string name="prefs_outro_thanks" translatable="false">
• Look4Sat 所有用户及贡献者!
\n• David A. B. Johnson (predict4java)
\n• Dave Moten (predict4java)
\n• Alexandru Csete (Gpredict)
\n• Dr T.S. Kelso (Celestrak)
\n• Libre Space Foundation (SatNOGS)
\n• xdsopl 和 Robot36 贡献者!
\n• BG7NTA BG5JSU(CW解码功能)
\n• BH2VSQ BG5JSU(天地图渲染支持)
\n• BG5JSU(LoTW上传支持)
</string>
<string name="prefs_outro_notice">Look4Sat-BA7OPF 是基于 Look4Sat(GPL-3.0)的非官方修改版,由 BA7OPF 独立维护,与原项目无隶属关系,亦未获其背书。
上游项目:https://github.com/rt-bishop/Look4Sat</string>
<string name="prefs_outro_license">基于 GNU GPL v3 许可 · 本程序不提供任何保修</string>
<!-- 匹配页 Mutual match page -->
<string name="mutual_title">过境匹配</string>
<string name="mutual_station_you">你的位置</string>
<string name="mutual_station_opposite">友台位置</string>
<string name="mutual_label_you">你</string>
<string name="mutual_label_opposite">友台</string>
<string name="mutual_status_calculating">正在计算双方共同可见窗口…</string>
<string name="mutual_status_error">请修正提示后重新查询</string>
<string name="mutual_status_matches">找到 %d 个匹配 · 点击卡片展开曲线</string>
<string name="mutual_status_none">未找到共同可见窗口,调整时间或仰角后重试</string>
<string name="mutual_status_idle">输入双方位置后点击查询</string>
<string name="mutual_chip_calculating">计算中</string>
<string name="mutual_chip_error">有错误</string>
<string name="mutual_chip_matches">%d 个匹配</string>
<string name="mutual_chip_none">无匹配</string>
<string name="mutual_chip_idle">待查询</string>
<string name="mutual_use_current">填入当前位置</string>
<string name="mutual_lat">纬度</string>
<string name="mutual_lon">经度</string>
<string name="mutual_grid">网格</string>
<string name="mutual_grid_hint">支持 4/6/8 位 Maidenhead 网格;也可直接输入经纬度</string>
<string name="mutual_min_elevation">最小仰角:%d°</string>
<string name="mutual_time_range">时间范围</string>
<string name="mutual_filter_transponder">转发器</string>
<string name="mutual_filter_fm">FM</string>
<string name="mutual_filter_linear">线性</string>
<string name="mutual_calculating">计算中…</string>
<string name="mutual_query">查询匹配</string>
<string name="mutual_elevation_you">你:%d°</string>
<string name="mutual_elevation_opposite">友台:%d°</string>
<string name="mutual_both_tracks">双方轨迹</string>
<string name="mutual_open_radar">查看雷达</string>
<string name="mutual_station_a">站点A</string>
<string name="mutual_station_b">站点B</string>
<string name="update_check_title">检查更新</string>
<string name="update_check_button">检查更新</string>
<string name="update_check_current">当前版本:%s</string>
<string name="update_check_latest">最新版本:%s</string>
<string name="update_check_up_to_date">已是最新版本</string>
<string name="update_check_available">发现新版本</string>
<string name="update_check_release_notes">更新内容</string>
<string name="update_check_download">下载并安装</string>
<string name="update_check_downloading">正在下载…</string>
<string name="update_check_error">检查更新失败,请检查网络连接。</string>
<string name="update_check_download_error">下载失败,请重试。</string>
<string name="update_check_no_apk">该版本未附带 APK 安装包。</string>
<!-- QSO edit dialog -->
<string name="qso_edit_title">编辑通联记录</string>
<string name="qso_edit_uploaded_notice">该通联已上传或已确认。保存将保留原记录不变,并新建一条记录。</string>
<string name="qso_edit_callsign">呼号</string>
<string name="qso_edit_date">日期 (UTC)</string>
<string name="qso_edit_time">时间 (UTC)</string>
<string name="qso_edit_tx">上行频率 (MHz)</string>
<string name="qso_edit_rx">下行频率 (MHz)</string>
<string name="qso_edit_band">频段:%1$s</string>
<string name="qso_edit_mode">模式</string>
<string name="qso_edit_satellite">卫星</string>
<string name="qso_edit_hint_satellite">LoTW 只接受它的官方卫星名</string>
<string name="qso_edit_sent">发送报告</string>
<string name="qso_edit_received">接收报告</string>
<string name="qso_edit_grid">对方网格</string>
<string name="qso_edit_comment">备注</string>
<string name="qso_edit_save">保存</string>
<string name="qso_edit_cancel">取消</string>
<string name="log_out_window_title">不在过境窗口</string>
<string name="log_out_window_message">当前时间不在该过境窗口内,将默认按过境中点 %1$s 录入;如需修改,请点击该条通联记录。</string>
<string name="log_out_window_confirm">录入</string>
<string name="qso_edit_err_callsign">呼号不能为空</string>
<string name="qso_edit_err_datetime">日期或时间格式不对 —— 应为 yyyy-MM-dd 与 HH:mm:ss(UTC)</string>
<string name="qso_edit_err_frequency">频率格式不对 —— 请输入 MHz,例如 145.850</string>
<string name="qso_edit_err_satellite">不是 LoTW 官方卫星名 —— 请从列表中选一个</string>
<string name="qso_edit_satellite_pick">选择</string>
<string name="qso_edit_satellite_pick_title">LoTW 官方卫星名</string>
<string name="qso_edit_satellite_search">搜索卫星名或数据源别名</string>
<string name="qso_edit_satellite_official">保存并签名时使用 %1$s</string>
<string name="qso_edit_warn_no_freq">没有上行频率:LoTW 要求 BAND,补上频率前这条无法上传</string>
<!-- 网格寻线(VUCC 网格线/网格角实地导航) -->
<string name="gridfinder_title">网格寻线</string>
<string name="gridfinder_status_corner">位于网格点上</string>
<string name="gridfinder_status_line">位于网格线上</string>
<string name="gridfinder_status_inside">在网格内部</string>
<string name="gridfinder_status_unknown">位置不可用</string>
<string name="gridfinder_status_waiting">正在获取定位…</string>
<string name="gridfinder_status_blocked">需要定位权限</string>
<string name="gridfinder_status_disabled">本机定位服务未开启</string>
<string name="gridfinder_status_confirmed">已由 %1$d 次连续满足 20 英尺规则的定位确认</string>
<string name="gridfinder_status_unconfirmed">尚未确认:精度达 6.1 m 的连续定位 %1$d/%2$d 次</string>
<string name="gridfinder_grant_permission">授予定位权限</string>
<string name="gridfinder_card_location">位置</string>
<string name="gridfinder_card_corner">最近网格点引导</string>
<string name="gridfinder_card_line">最近网格线引导</string>
<string name="gridfinder_card_lines">两条边界线</string>
<string name="gridfinder_card_map">近距离网格图</string>
<string name="gridfinder_card_fix">定位质量</string>
<string name="gridfinder_card_compass">罗盘</string>
<string name="gridfinder_label_claimable">此处可计分的网格</string>
<string name="gridfinder_label_grid4">网格 (4)</string>
<string name="gridfinder_label_grid6">网格 (6)</string>
<string name="gridfinder_label_grid8">网格 (8)</string>
<string name="gridfinder_label_latitude">纬度</string>
<string name="gridfinder_label_longitude">经度</string>
<string name="gridfinder_label_altitude">海拔</string>
<string name="gridfinder_label_accuracy">水平精度</string>
<string name="gridfinder_label_fix_age">定位时刻</string>
<string name="gridfinder_label_provider">定位来源</string>
<string name="gridfinder_value_provider_on">已开启</string>
<string name="gridfinder_value_provider_off">未开启</string>
<string name="gridfinder_label_distance">距离</string>
<string name="gridfinder_label_bearing">方位角</string>
<string name="gridfinder_label_direction">方向</string>
<string name="gridfinder_label_grids">网格</string>
<string name="gridfinder_label_lat_line">纬线</string>
<string name="gridfinder_label_lon_line">经线</string>
<string name="gridfinder_line_value">位于%2$s方向 %1$s</string>
<string name="gridfinder_line_latitude">这是纬度边界,东西走向</string>
<string name="gridfinder_line_longitude">这是经度边界,南北走向</string>
<string name="gridfinder_label_heading">朝向</string>
<string name="gridfinder_label_compass_accuracy">罗盘精度</string>
<string name="gridfinder_action_set_station">设为我的台址</string>
<string name="gridfinder_action_station_saved">台址已更新 —— 过境预报将使用这个位置</string>
<string name="gridfinder_action_calibrate">校准罗盘</string>
<string name="gridfinder_map_legend">中心点是你的位置,虚线是正北与正东,直线是两条网格边界,绿点是最近的网格点,圆环是最近网格线的垂足,红色指针是你的朝向</string>
<string name="gridfinder_compass_disabled_hint">设置里未开启罗盘,表盘按正北朝上显示。方位角数字仍可引导你</string>
<string name="gridfinder_compass_unavailable_hint">本机没有磁力传感器 —— 请按方位角数字导航</string>
<string name="gridfinder_compass_hint">请平持手机,箭头指向应走的方向</string>
<string name="gridfinder_vucc_rule_note">VUCC 20 英尺规则:%1$.1f m</string>
<string name="gridfinder_direction_north">北</string>
<string name="gridfinder_direction_south">南</string>
<string name="gridfinder_direction_east">东</string>
<string name="gridfinder_direction_west">西</string>
<string name="gridfinder_action_set_lotw_station">设为 LoTW 上传台址(%1$s)</string>
<string name="gridfinder_lotw_station_hint">预填上传台站网格:上传时 LoTW 按这些格记录(线上 2 格、角上 4 格);证书呼号保持不变。</string>
</resources>
@@ -0,0 +1,91 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.core.presentation
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.w3c.dom.Element
/**
* Android 7+ resolves Chinese by script, so an unqualified values-zh is treated as
* zh-Hans and a Traditional device (zh-TW / zh-HK / zh-MO, i.e. zh-Hant) never matches
* it — it used to fall back to the English default. values-b+zh+Hant carries the
* Simplified text for those devices, and the locale filter in the convention plugin
* keeps that config in the APK.
*
* Both halves fail silently at runtime (English strings, or a Hant config dropped from
* the resource table), so they are pinned here: keep the Hant copy verbatim and keep
* the Traditional locales in the locale filter.
*/
class TraditionalChineseFallbackTest {
private val simplified = stringEntries("src/main/res/values-zh/strings.xml")
private val traditional = stringEntries("src/main/res/values-b+zh+Hant/strings.xml")
private val conventionPlugin = generateSequence(File("").absoluteFile) { it.parentFile }
.map { File(it, "build-logic/convention/src/main/java/com/rtbishop/look4sat/convention/PluginSetupUtils.kt") }
.first { it.isFile }
@Test
fun `hant fallback is a verbatim copy of the simplified strings`() {
assertTrue("values-zh/strings.xml looks empty or unparsable", simplified.size > 400)
val missing = (simplified.keys - traditional.keys).sorted()
val extra = (traditional.keys - simplified.keys).sorted()
val changed = simplified.keys.intersect(traditional.keys)
.filter { simplified[it] != traditional[it] }
.sorted()
assertEquals("keys missing from values-b+zh+Hant", emptyList<String>(), missing)
assertEquals("keys only present in values-b+zh+Hant", emptyList<String>(), extra)
assertEquals("values-b+zh+Hant diverged from values-zh (copy the change over)", emptyList<String>(), changed)
}
@Test
fun `traditional locales survive the locale filter`() {
assertTrue("convention plugin not found: ${conventionPlugin.absolutePath}", conventionPlugin.isFile)
// Comments may mention either spelling, so only the code lines are inspected.
val filters = conventionPlugin.readLines()
.filterNot { it.trimStart().startsWith("//") }
.joinToString("\n")
val traditionalLocales = listOf("zh-rTW", "zh-rHK", "zh-rMO")
val dropped = traditionalLocales.filterNot { filters.contains("\"$it\"") }
assertEquals(
"localeFilters must list the Traditional locales, otherwise aapt2 strips" +
" values-b+zh+Hant from the APK and Traditional devices get English",
emptyList<String>(),
dropped
)
// aapt2 rejects the script spelling, so it must not creep back into the filter.
assertTrue("localeFilters must use the aapt2 'zh-rTW' spelling, not 'zh-Hant'", !filters.contains("\"zh-Hant\""))
}
private fun stringEntries(path: String): Map<String, String> {
val file = File(path)
assertTrue("resource file not found: ${file.absolutePath}", file.isFile)
val document = DocumentBuilderFactory.newInstance()
.apply { isNamespaceAware = false }
.newDocumentBuilder()
.parse(file)
val nodes = document.getElementsByTagName("string")
return (0 until nodes.length).associate {
val element = nodes.item(it) as Element
element.getAttribute("name") to element.textContent.trim()
}
}
}
@@ -82,7 +82,10 @@ class MutualViewModel(
private val settingsRepo: ISettingsRepo,
// Injected so unit tests can run the pass computation on a test dispatcher;
// production callers keep the CPU-bound pool default.
private val computeDispatcher: CoroutineDispatcher = Dispatchers.Default
private val computeDispatcher: CoroutineDispatcher = Dispatchers.Default,
// Injected so unit tests can pin the wall clock to the fixture TLE epoch and
// stay deterministic; production callers keep the real clock default.
private val nowMillis: () -> Long = System::currentTimeMillis
) : ViewModel() {
private val _uiState = MutableStateFlow(MutualUiState())
@@ -303,7 +306,7 @@ class MutualViewModel(
queryGeneration += 1
viewModelScope.launch {
val now = System.currentTimeMillis()
val now = nowMillis()
// 转发器类型筛选: 使用硬编码 AMSAT Live 清单 (见 Sources):
// FM = {SO-50, ISS ZARYA, AO-123}, Linear = {RS-44, FO-29,
// AO-7, AO-73, JO-97}. 不再依赖 AMSAT 网页同步: 任何网络下都
@@ -53,7 +53,9 @@ class MutualViewModelTest {
): MutualViewModel = MutualViewModel(
satelliteRepo = FakeSatelliteRepo(satellites, passes),
settingsRepo = FakeSettingsRepo(position),
computeDispatcher = StandardTestDispatcher(mainDispatcherRule.dispatcher.scheduler)
computeDispatcher = StandardTestDispatcher(mainDispatcherRule.dispatcher.scheduler),
// Pin the clock to the fixture base so results never depend on wall time.
nowMillis = { TestOrbits.FIXTURE_BASE }
)
private fun TestScope.queryAndSettle(vm: MutualViewModel) {
@@ -219,7 +221,7 @@ class MutualViewModelTest {
assertTrue(state.mutualPasses.all { it.catNum == 25544 })
assertTrue(state.mutualPasses.all { it.name == "ISS (ZARYA)" })
// AOS/LOS within the searched horizon
val now = System.currentTimeMillis()
val now = TestOrbits.FIXTURE_BASE
assertTrue(state.mutualPasses.all { it.startTime >= now - 120_000L })
assertTrue(state.mutualPasses.all { it.endTime <= now + 12 * 3600_000L + 120_000L })
}
@@ -48,6 +48,15 @@ object TestOrbits {
)
)
/**
* Fixed wall-clock base for every fixture computation: 2024-03-10T00:00Z,
* ~18 h after [ISS]'s TLE epoch, so SGP4 propagates an accurate, healthy
* orbit. Tests pin the ViewModel clock to this value as well
* (MutualViewModel.nowMillis), which makes pass windows deterministic
* instead of rotting as the fixture ages against System.currentTimeMillis().
*/
val FIXTURE_BASE: Long = 1710028800000L
/** Guangzhou (default BA7OPF grid OL62/OL63 area). */
val GUANGZHOU = GeoPos(23.13, 113.26)
@@ -85,7 +94,7 @@ object TestOrbits {
fun findPassWindows(
sat: OrbitalObject = ISS,
pos: GeoPos = GUANGZHOU,
time: Long = System.currentTimeMillis(),
time: Long = FIXTURE_BASE,
hoursAhead: Int = 12
): List<OrbitalPass> {
if (!sat.willBeSeen(pos)) return emptyList()
+2 -2
View File
@@ -1,8 +1,8 @@
[versions]
#noinspection UnusedVersionCatalogEntry
appVersionCode = "580"
appVersionCode = "583"
#noinspection UnusedVersionCatalogEntry
appVersionName = "4.4.8-ba7opf.11.5"
appVersionName = "4.4.8-ba7opf.11.8"
#noinspection UnusedVersionCatalogEntry
compileSdk = "37"
#noinspection UnusedVersionCatalogEntry