diff --git a/build-logic/convention/src/main/java/com/rtbishop/look4sat/convention/PluginSetupUtils.kt b/build-logic/convention/src/main/java/com/rtbishop/look4sat/convention/PluginSetupUtils.kt index 334a9308..adea0394 100644 --- a/build-logic/convention/src/main/java/com/rtbishop/look4sat/convention/PluginSetupUtils.kt +++ b/build-logic/convention/src/main/java/com/rtbishop/look4sat/convention/PluginSetupUtils.kt @@ -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/*") } } } diff --git a/core/presentation/src/main/res/values-b+zh+Hant/strings.xml b/core/presentation/src/main/res/values-b+zh+Hant/strings.xml new file mode 100644 index 00000000..9c2ab235 --- /dev/null +++ b/core/presentation/src/main/res/values-b+zh+Hant/strings.xml @@ -0,0 +1,529 @@ + + + + + 确定 + 取消 + 没有数据可显示 + + + 卫星 + 过境 + 雷达 + 匹配 + 地图 + 设置 + + + 类型:%s + 选择卫星类型 + Id - 名称 + 清空 + 全部清空 + 全选 + 已选择 + 可用 + 已选择(%1$d) + 可用(%1$d) + 请确保您的搜索查询正确且数据库已更新 + 警告\! + 此应用收录了超过9000颗卫星信息\n同时追踪所有卫星并不现实\n建议始终通过搜索和分类筛选功能仅勾选您感兴趣的卫星 + + + 筛选过境 + 仰角值 + 提前时间 + 历史过境时间 + AOS 时间段 + 反选 AOS 时间段 + DeepSpace(周期 >225分钟) + 选择调制类型 + 仰角:%.1f° + 高度:%d km + 请确保您的过滤器设置正确,并且已选择卫星 + 没有即将过境的卫星 + 欢迎来到 Look4Sat\! + 请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性 + + + +AMSAT + AMSAT 卫星状态 + 刷新 + 重试 + 更新于: + 活跃 + 遥测/信标 + 未听到 + 冲突 + 名称 + 加载失败 - 检查网络后重试 + 此时间段无报告 + 关闭 + 上传 + 返回 + 上传报告到 AMSAT + 状态 + 呼号 + 网格 + 时间:当前 UTC %s + 提交到 AMSAT + 确认上传 + 确认上传到 AMSAT + 这条公开报告将上传到 AMSAT。\n卫星:%1$s\n状态:%2$s\n呼号:%3$s\n网格:%4$s + 未填写 + 报告已上传 + 上传失败:%s + 请填写呼号 + 网格应为 4 或 6 位 Maidenhead 定位符 + 活跃 + 遥测/信标 + 未听到 + ISS 语音 + + + 后退 + 通知 + 方位角 + 仰角 + 高度 + 距离 + 地影中 + 下行链路 + 上行链路 + 这颗卫星没有收发器 + 添加 + 卫星收发器: + 倒置:%s + 调制:%s + 否 + 是 + 日照中 + + 多普勒频率计算器 + 输入上行频率 (MHz) + 输入下行频率 (MHz) + 偏移 (kHz) + 线性转发器:输入一个频率即可算出另一个 + + CW 解码器 + 开始 + 停止 + 清空 + + + 上一个 + 下一个 + 网格模式 + 卫星模式 + 首通呼号 + 方位角:%.1f° + 仰角:%.1f° + 高度:%.0f km + 距离:%.0f km + 纬度:%.1f° + 经度:%.1f° + 相位:%.1f° + 地影中 + 周期:% .0f min + 速度:%.2f km/s + 能见度:%s + + %d 个呼号 · %d 次通联 + 该区域暂无已确认通联 + %d QSO + %d QSOs + 首通 + 卫星未知 + 匹配 + + 标记你想通联的电台 + 呼号 + 标记 + 标记 + 删除标记 + 置顶 + 日照中 + + + 隐私政策 + + 更新:%s + yyyy年M月d日 HH:mm:ss + + 站点位置 + GPS 定位 + 检查您的位置权限 + 输入位置 + 输入的位置无效 + QTH 网格 + 无效的 QTH 网格 + 位置更新成功 + 站点位置设置 + 输入站点的纬度 + 输入站点的经度 + QTH 网格 + 配置 + Wavelog 地址 + 同步中… + Wavelog 配置 + 与 LoTW 二选一配置,两者不兼容。 + 增量同步 + 全量同步 + 清除配置 + 清除 Wavelog 配置? + 清除后将切回 LoTW 模式,LoTW 功能恢复可用。 + 配置提醒 + 已配置 LoTW。配置 Wavelog 后,App 内 LoTW 的上传与同步将不可用(两者二选一,不兼容)。继续? + 继续 + Wavelog 模式中不可用(二选一) + 未配置 — 把本机通联记录上传到 Wavelog + 站点 %1$s — 可以上传 + API key(读写权限) + 获取站点 + 检测中… + 未选择站点 — 点「获取站点」 + 密钥:读写 — 可上传 + 密钥只读 — 请在 Wavelog → API 生成「读写」密钥 + 点「获取站点」选择上传使用的台站档案;密钥需要「读写」权限。 + LoTW 已确认网格 + LoTW 呼号 + LoTW 密码 + 直接从 ARRL LoTW 拉取该呼号已确认 QSO 的网格并覆盖本地已存集合(当前 %1$d 个)。密码仅保存在手机本地。点"同步"会自动保存凭据并拉取。 + 同步 + 同步中… + 已从 LoTW 同步 — 共 %1$d 个网格 + 未配置 — 直接登录 ARRL LoTW 拉取已确认 QSO 的网格,无需自建服务器 + LoTW 呼号/密码未配置 + LoTW 同步失败——呼号或密码错误 + LoTW 同步失败——服务器限流,请等几分钟后再试 + LoTW 同步失败——连接超时,请更换网络重试 + LoTW 同步失败——网络错误(%1$s) + 全量同步 + 增量合并 + 下载速度约每 100 条 QSO 需 8 秒(另有 5–40 秒服务器生成时间) + 正在连接 LoTW 服务器… + 正在下载报告… + 正在处理下载的数据… + 本次同步 %1$d 条 QSO… + 本次同步 %1$d 条 QSO,预计还需 %2$d 秒 + 使用梅登黑德网格设置站点位置 + + 删除 + 日志本 + %1$d 条记录 — 本地通联与 LoTW 确认 + 暂无记录 — 在雷达页 Log 标签记录通联 + 关闭 + 上传 + 上传到 LoTW + 确认上传 + 重传选中 (%1$d) + 已选 %1$d 条 + 长按记录行可多选重传 + 已上传记录将按当前台址就地更新;已被奖项使用的旧网格可能被拒绝。 + 其中 %1$d 条记录没有呼号,将按证书 %2$s 上传。 + %1$d 条记录登记的不是证书 %2$s 的呼号。 + 改写为 %1$s + 换一张证书 + 台址 + 全部 + 上传到 Wavelog + 上传前核对 + 修改台址 + 忽略 + 当前定位 %1$s 不在证书台址网格 %2$s 内——如果正在此处操作,请先更新台址再上传。 + 当前定位 %1$s 不在证书台址网格 %2$s 内 · 点此更新 + LoTW 上传证书 + 证书已导入 · 台址网格 %1$s + 未配置 — 导入 TrustedQSL 证书后即可上传通联 + 处理中… + 导入你的 TrustedQSL .p12 证书(与 TQSL 同款)用于签名上传通联。密码仅加密保存在手机本地。 + 证书密码 + 证书无密码时留空即可 + 选择 .p12 文件 + 导入 + 证书密码错误,请检查 TQSL 导出 .p12 时设置的密码 + 证书已过期或尚未生效 + 不是有效的 LoTW 证书文件 + 不支持的 .p12 加密,请用 TQSL 默认加密重新导出后再试 + 不支持的 .p12 加密(%1$s),请用 TQSL 默认加密重新导出后再试 + 导入失败,请重试 + %1$s · DXCC %2$d · 有效期至 %3$s + 移除证书 + 台址 + 未选择 + 省份 + 导入证书后可选 + 网格(逗号分隔,可多格) + 保存 + 关闭 + + 卫星数据更新 + 卫星:%s + 收发器:%s + 在线更新 + 导入文件 + 清空 + 数据清空成功 + 更新成功 + + 未导入卫星。请选择有效的 TLE/3LE (.txt) 或 OMM (.csv) 文件。 + 未导入收发器。请选择有效的 SatNOGS (.json) 文件。 + + 自定义数据源 + 轨道数据优先取列表最新星历,此顺序仅影响卫星名称 + 靠前优先:收发器数据 + 恢复默认数据源 + 自定义TLE URL + 自定义收发器URL + URL + 卫星数据 + 收发器数据 + + 数据输出 + 网络 + 蓝牙 + CAT + + 网络数据输出 + 启用方位俯仰角输出 + IP:端口 + 数据格式 + 启用多普勒频率输出 + IP:端口 + 数据格式 + 频率偏移 (Hz) + 范围:-50000 到 50000 Hz。正值提高上报频率,负值降低上报频率。 + + 蓝牙数据输出 + 启用方位俯仰角输出 + 蓝牙设备 ID + 数据格式 + 启用多普勒频率输出 + 蓝牙设备 ID + 数据格式 + 请检查蓝牙权限 + 请检查网络权限 + + + 电台控制 + CAT 电台控制 + 电台型号 + 发射电台蓝牙地址 + 接收电台蓝牙地址 + 发射电台名称 + 接收电台名称 + 启用 CAT 控制 + + 其他设置 + 地图设置 + 配置地图 + 地图来源 + 离线 OSM 地图 + 天地图(矢量) + 天地图(影像) + 天地图访问密钥 + 使用天地图前,需先在天地图开发者控制台申请服务端应用 Key。 + 选择天地图需要填写访问密钥;保存密钥前将继续使用离线 OSM 地图。 + 以 UTC 时间显示 + 启用卫星数据自动更新 + 自动同步 LoTW 已确认网格 + 启用雷达扫描动画 + 使用传感器旋转雷达视图 + 校准指南针 + 指南针校准 + 缓慢移动手机画“8”字,直到精度提高。请远离磁铁和金属物体。 + 精度:%s + 高——校准完成 + 中——请继续移动 + 低——请继续移动 + 不可靠——请开始“8”字动作 + 设备没有可用的磁场传感器 + 校正后航向:%.1f° + 手动航向偏置(°) + 范围 −180° 至 +180°;正值为顺时针偏移。 + 偏置归零 + 浅色主题 + 启用红色夜间模式 + 地图显示 Maidenhead 网格 + + 仰角高亮 + 低 < %1$d° + 中 %1$d-%2$d° + 高 > %1$d° + + 致谢与声明 + Project Wiki + Look4Sat-BA7OPF 是基于 Look4Sat 的定制版,保留上游全部功能,并新增: + 卫星日志本与 LoTW + 卫星通联日志本——通联时一键记录呼号、时间、频率、模式、信号报告和网格。 + LoTW 直传——导入 ARRL 证书后,直接在 App 内把日志上传到 LoTW,无需电脑。 + 确认同步——一键同步 LoTW 官方确认,日志自动标记“已上传 / 已确认”;修改已上传的记录会自动新建一条,原记录不受影响。 + 用法:通联后点“记录”→ 日志页点“上传”→ 设置里“同步”查看确认。 + 双站过境匹配 + 友台共视匹配——输入你和友台的位置,找出双方同时可见的卫星过境,点一下直接进雷达图。 + 用法:Match 页 → 设置两站位置 → 查询。 + 通联网格地图 + 已通联网格——LoTW/Wavelog 同步的网格在地图上标绿,支持奖状边界和首通呼号标签;点格子看该网格的通联记录,还能一键去匹配友台。 + 用法:地图页开启网格模式 → 设置里同步 LoTW/Wavelog。 + + • 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上传支持) + + Look4Sat-BA7OPF 是基于 Look4Sat(GPL-3.0)的非官方修改版,由 BA7OPF 独立维护,与原项目无隶属关系,亦未获其背书。 +上游项目:https://github.com/rt-bishop/Look4Sat +基于 GNU GPL v3 许可 · 本程序不提供任何保修 + + + 过境匹配 + 你的位置 + 友台位置 + 你 + 友台 + 正在计算双方共同可见窗口… + 请修正提示后重新查询 + 找到 %d 个匹配 · 点击卡片展开曲线 + 未找到共同可见窗口,调整时间或仰角后重试 + 输入双方位置后点击查询 + 计算中 + 有错误 + %d 个匹配 + 无匹配 + 待查询 + 填入当前位置 + 纬度 + 经度 + 网格 + 支持 4/6/8 位 Maidenhead 网格;也可直接输入经纬度 + 最小仰角:%d° + 时间范围 + 转发器 + FM + 线性 + 计算中… + 查询匹配 + 你:%d° + 友台:%d° + 双方轨迹 + 查看雷达 + 站点A + 站点B + 检查更新 + 检查更新 + 当前版本:%s + 最新版本:%s + 已是最新版本 + 发现新版本 + 更新内容 + 下载并安装 + 正在下载… + 检查更新失败,请检查网络连接。 + 下载失败,请重试。 + 该版本未附带 APK 安装包。 + + + 编辑通联记录 + 该通联已上传或已确认。保存将保留原记录不变,并新建一条记录。 + 呼号 + 日期 (UTC) + 时间 (UTC) + 上行频率 (MHz) + 下行频率 (MHz) + 频段:%1$s + 模式 + 卫星 + LoTW 只接受它的官方卫星名 + 发送报告 + 接收报告 + 对方网格 + 备注 + 保存 + 取消 + 不在过境窗口 + 当前时间不在该过境窗口内,将默认按过境中点 %1$s 录入;如需修改,请点击该条通联记录。 + 录入 + 呼号不能为空 + 日期或时间格式不对 —— 应为 yyyy-MM-dd 与 HH:mm:ss(UTC) + 频率格式不对 —— 请输入 MHz,例如 145.850 + 不是 LoTW 官方卫星名 —— 请从列表中选一个 + 选择 + LoTW 官方卫星名 + 搜索卫星名或数据源别名 + 保存并签名时使用 %1$s + 没有上行频率:LoTW 要求 BAND,补上频率前这条无法上传 + + + 网格寻线 + 位于网格点上 + 位于网格线上 + 在网格内部 + 位置不可用 + 正在获取定位… + 需要定位权限 + 本机定位服务未开启 + 已由 %1$d 次连续满足 20 英尺规则的定位确认 + 尚未确认:精度达 6.1 m 的连续定位 %1$d/%2$d 次 + 授予定位权限 + 位置 + 最近网格点引导 + 最近网格线引导 + 两条边界线 + 近距离网格图 + 定位质量 + 罗盘 + 此处可计分的网格 + 网格 (4) + 网格 (6) + 网格 (8) + 纬度 + 经度 + 海拔 + 水平精度 + 定位时刻 + 定位来源 + 已开启 + 未开启 + 距离 + 方位角 + 方向 + 网格 + 纬线 + 经线 + 位于%2$s方向 %1$s + 这是纬度边界,东西走向 + 这是经度边界,南北走向 + 朝向 + 罗盘精度 + 设为我的台址 + 台址已更新 —— 过境预报将使用这个位置 + 校准罗盘 + 中心点是你的位置,虚线是正北与正东,直线是两条网格边界,绿点是最近的网格点,圆环是最近网格线的垂足,红色指针是你的朝向 + 设置里未开启罗盘,表盘按正北朝上显示。方位角数字仍可引导你 + 本机没有磁力传感器 —— 请按方位角数字导航 + 请平持手机,箭头指向应走的方向 + VUCC 20 英尺规则:%1$.1f m + 北 + 南 + 东 + 西 + 设为 LoTW 上传台址(%1$s) + 预填上传台站网格:上传时 LoTW 按这些格记录(线上 2 格、角上 4 格);证书呼号保持不变。 + + diff --git a/core/presentation/src/test/java/com/rtbishop/look4sat/core/presentation/TraditionalChineseFallbackTest.kt b/core/presentation/src/test/java/com/rtbishop/look4sat/core/presentation/TraditionalChineseFallbackTest.kt new file mode 100644 index 00000000..7fc9f22a --- /dev/null +++ b/core/presentation/src/test/java/com/rtbishop/look4sat/core/presentation/TraditionalChineseFallbackTest.kt @@ -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 . + */ +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(), missing) + assertEquals("keys only present in values-b+zh+Hant", emptyList(), extra) + assertEquals("values-b+zh+Hant diverged from values-zh (copy the change over)", emptyList(), 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(), + 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 { + 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() + } + } +}