Skip to content

Commit 0d08f88

Browse files
committed
fix(i18n): name regulatory ranges and pipeline checks in Chinese
A refuting review found English left in the zh "why" panel: a regulatory range block stored its range name as an English literal ("Spain 400 commercial call range"), and Lookup's check list had no zh names. RegulatoryPrefix now carries a string resource for the range, resolved when the checker runs, and the 37 check names are translated. Test changes: the zh sweep only flagged three lowercase English words in a row, so a two-word leak ("Spam database") or a single word passed. It now flags any Latin word outside the names the translation keeps, renders every code with a Chinese description so labeled bullets are exercised, and was mutation-checked (dropping one zh check name fails it). The identity-evidence expectation changes because the zh wording for an unverified attestation said verification failed, when the code only knows the carrier's status wasn't PASS.
1 parent 2643ffd commit 0d08f88

8 files changed

Lines changed: 111 additions & 20 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,12 @@ The rest of this section takes effect when the report Worker is next deployed.
508508
category the way the category settings do ("Debt Collector", "Wangiri
509509
Scam"), and an RCS block names its underlying check the way the block log
510510
does.
511+
- A few English words still showed up in that panel on a Chinese phone. A
512+
block by one of the country telemarketing ranges named the range in
513+
English ("Spain 400 commercial call range"), and Lookup's list of checks
514+
used the English check names. Both are Chinese now. The Chinese line for
515+
an unverified PASSporT attestation also said verification had failed, when
516+
all CallShield knows is that the carrier didn't report a pass.
511517

512518
- A shipped locale can no longer decay silently. Each has a coverage floor in
513519
`scripts/translation_floors.json`, and dropping below it fails the build.

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ language in [issue #7](https://github.com/SysAdminDoc/CallShield/issues/7).
510510
| URL Safety | Local spam-domain data; optional URLhaus (abuse.ch) |
511511
| Verification | Local Gradle, lint, and release-artifact checks |
512512
| Tests | 1100 JVM unit tests (JUnit) |
513-
| Strings | 1567 string resources and 37 plural groups (translation-ready) |
513+
| Strings | 1571 string resources and 37 plural groups (translation-ready) |
514514
| Accessibility | 100+ content descriptions, 48dp touch targets |
515515
| Min SDK | 29 (Android 10) |
516516
| Target SDK | 36 |

‎app/src/main/java/com/sysadmindoc/callshield/data/RegulatoryPrefix.kt‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ enum class RegulatoryPrefix(
2424
val e164Prefix: String,
2525
val nationalPrefix: String,
2626
val nationalLength: Int,
27-
/** Stored with the decision and shown as the range that matched. */
28-
val description: String,
27+
/** The range's name, shown with the decision as the range that matched. */
28+
val descriptionRes: Int,
2929
val titleRes: Int,
3030
val summaryRes: Int,
3131
) {
@@ -36,7 +36,7 @@ enum class RegulatoryPrefix(
3636
e164Prefix = "+34400",
3737
nationalPrefix = "400",
3838
nationalLength = 9,
39-
description = "Spain 400 commercial call range",
39+
descriptionRes = R.string.reg_prefix_spain_400_match,
4040
titleRes = R.string.reg_prefix_spain_400,
4141
summaryRes = R.string.reg_prefix_spain_400_desc,
4242
),
@@ -47,7 +47,7 @@ enum class RegulatoryPrefix(
4747
e164Prefix = "+91140",
4848
nationalPrefix = "140",
4949
nationalLength = 10,
50-
description = "India 140 promotional call series",
50+
descriptionRes = R.string.reg_prefix_india_140_match,
5151
titleRes = R.string.reg_prefix_india_140,
5252
summaryRes = R.string.reg_prefix_india_140_desc,
5353
),
@@ -58,7 +58,7 @@ enum class RegulatoryPrefix(
5858
e164Prefix = "+55303",
5959
nationalPrefix = "0303",
6060
nationalLength = 11,
61-
description = "Brazil 0303 telemarketing code",
61+
descriptionRes = R.string.reg_prefix_brazil_0303_match,
6262
titleRes = R.string.reg_prefix_brazil_0303,
6363
summaryRes = R.string.reg_prefix_brazil_0303_desc,
6464
),
@@ -69,7 +69,7 @@ enum class RegulatoryPrefix(
6969
e164Prefix = "+911600",
7070
nationalPrefix = "1600",
7171
nationalLength = 10,
72-
description = "India 1600 series for banks, insurers and government offices",
72+
descriptionRes = R.string.reg_prefix_india_1600_allow_match,
7373
titleRes = R.string.reg_prefix_india_1600_allow,
7474
summaryRes = R.string.reg_prefix_india_1600_allow_desc,
7575
),

‎app/src/main/java/com/sysadmindoc/callshield/data/checker/Checkers.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ internal class RegulatoryPrefixChecker(
470470

471471
override suspend fun check(ctx: CheckContext): BlockResult? =
472472
RegulatoryPrefix.enabledMatch(ctx.prefs, ctx.number, allows = false) { homeRegion(ctx.appContext) }?.let { range ->
473-
BlockResult.block(name, "telemarketer", range.description)
473+
BlockResult.block(name, "telemarketer", ctx.appContext.getString(range.descriptionRes))
474474
}
475475
}
476476

@@ -485,7 +485,7 @@ internal class RegulatoryAllowChecker(
485485

486486
override suspend fun check(ctx: CheckContext): BlockResult? =
487487
RegulatoryPrefix.enabledMatch(ctx.prefs, ctx.number, allows = true) { homeRegion(ctx.appContext) }?.let { series ->
488-
BlockResult.allow(name, description = series.description)
488+
BlockResult.allow(name, description = ctx.appContext.getString(series.descriptionRes))
489489
}
490490
}
491491

‎app/src/main/res/values-zh-rCN/strings.xml‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1061,6 +1061,47 @@
10611061
<string name="reg_prefix_brazil_0303_desc">巴西的电话营销来电使用 ANATEL 为其指定的 0303 代码。</string>
10621062
<string name="reg_prefix_india_1600_allow">保护印度 1600 号段</string>
10631063
<string name="reg_prefix_india_1600_allow_desc">印度 1600 号段专供银行、保险公司和政府机构使用,来自该号段的来电不会被垃圾号码库拦截。</string>
1064+
<string name="lookup_checker_manual_whitelist">已保存的安全号码</string>
1065+
<string name="lookup_checker_emergency_floor">紧急号码保护</string>
1066+
<string name="lookup_checker_otp_floor">验证码保护</string>
1067+
<string name="lookup_checker_contact_whitelist">联系人</string>
1068+
<string name="lookup_checker_contacts_only">仅联系人模式</string>
1069+
<string name="lookup_checker_stir_shaken_trusted">已验证的来电者</string>
1070+
<string name="lookup_checker_stir_shaken_failed">来电者验证</string>
1071+
<string name="lookup_checker_temporary_allow">临时允许</string>
1072+
<string name="lookup_checker_system_block_list">系统拦截列表</string>
1073+
<string name="lookup_checker_user_blocklist">CallShield 拦截列表</string>
1074+
<string name="lookup_checker_database">垃圾号码库</string>
1075+
<string name="lookup_checker_db_prefix_expansion">垃圾号码库前缀</string>
1076+
<string name="lookup_checker_prefix">前缀规则</string>
1077+
<string name="lookup_checker_regulatory_prefix">电话营销号段</string>
1078+
<string name="lookup_checker_regulatory_allow">受保护号段</string>
1079+
<string name="lookup_checker_wildcard">通配符规则</string>
1080+
<string name="lookup_checker_hash_wildcard">号码模式规则</string>
1081+
<string name="lookup_checker_recently_dialed">最近拨打过</string>
1082+
<string name="lookup_checker_answered_caller">曾接听过</string>
1083+
<string name="lookup_checker_emergency_callback">紧急回拨</string>
1084+
<string name="lookup_checker_repeated_urgent">重复的紧急来电</string>
1085+
<string name="lookup_checker_caller_name_trust">受信任的主叫姓名</string>
1086+
<string name="lookup_checker_caller_name">主叫姓名规则</string>
1087+
<string name="lookup_checker_region_block">地区规则</string>
1088+
<string name="lookup_checker_campaign_recorder">骚扰活动监测</string>
1089+
<string name="lookup_checker_time_block">勿扰时段</string>
1090+
<string name="lookup_checker_frequency">重复来电</string>
1091+
<string name="lookup_checker_heuristic">行为分析</string>
1092+
<string name="lookup_checker_campaign_burst">骚扰活动</string>
1093+
<string name="lookup_checker_ml_scorer">设备端垃圾来电模型</string>
1094+
<string name="lookup_checker_meeting_mode">会议模式</string>
1095+
<string name="lookup_checker_sms_context">消息信任</string>
1096+
<string name="lookup_checker_sms_burst">消息爆发</string>
1097+
<string name="lookup_checker_keyword">关键词规则</string>
1098+
<string name="lookup_checker_sms_content">消息内容</string>
1099+
<string name="lookup_checker_push_alert">推送提醒报告</string>
1100+
<string name="lookup_checker_other">其他筛查检查</string>
1101+
<string name="reg_prefix_spain_400_match">西班牙 400 商业来电号段</string>
1102+
<string name="reg_prefix_india_140_match">印度 140 推广来电号段</string>
1103+
<string name="reg_prefix_brazil_0303_match">巴西 0303 电话营销代码</string>
1104+
<string name="reg_prefix_india_1600_allow_match">印度 1600 号段(银行、保险公司和政府机构专用)</string>
10641105
<string name="region_rules_title">地区与主叫姓名规则</string>
10651106
<string name="region_rules_disclosure">这些规则完全在你的设备上运行。地区拦截对北美号码读取区号,对其他号码读取国家代码。主叫姓名信任仅使用 Android 暴露给筛查服务的运营商呈现姓名。</string>
10661107
<string name="region_rules_block_outside">拦截允许地区之外的来电</string>
@@ -1106,7 +1147,7 @@
11061147
<string name="identity_evidence_summary">身份证据:%1$s</string>
11071148
<string name="identity_evidence_separator">;</string>
11081149
<string name="identity_evidence_attestation">PASSporT %1$s 级认证元数据</string>
1109-
<string name="identity_evidence_attestation_unverified">PASSporT %1$s 级认证元数据(运营商验证未通过)</string>
1150+
<string name="identity_evidence_attestation_unverified">PASSporT %1$s 级认证元数据(运营商状态不是“通过”)</string>
11101151
<string name="identity_evidence_dno_listed">来电号码在禁止呼出(DNO)名单中</string>
11111152
<string name="identity_evidence_unassigned_origin">来电号码尚未分配</string>
11121153
<string name="identity_evidence_line_voip">线路类型:网络电话</string>

‎app/src/main/res/values/strings.xml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1586,6 +1586,10 @@
15861586
<string name="reg_prefix_brazil_0303_desc">Telemarketers in Brazil call from the 0303 code that ANATEL set aside for them.</string>
15871587
<string name="reg_prefix_india_1600_allow">Protect India 1600 series</string>
15881588
<string name="reg_prefix_india_1600_allow_desc">India\'s 1600 series belongs to banks, insurers and government offices, so calls from it ring through past the spam database.</string>
1589+
<string name="reg_prefix_spain_400_match">Spain 400 commercial call range</string>
1590+
<string name="reg_prefix_india_140_match">India 140 promotional call series</string>
1591+
<string name="reg_prefix_brazil_0303_match">Brazil 0303 telemarketing code</string>
1592+
<string name="reg_prefix_india_1600_allow_match">India 1600 series for banks, insurers and government offices</string>
15891593
<string name="region_rules_title">Region &amp; caller-name rules</string>
15901594
<string name="region_rules_disclosure">These rules run entirely on your device. Region blocking reads the area code of a North American number and the country code of any other. Caller-name trust uses only a carrier-presented name that Android exposes to the screening service.</string>
15911595
<string name="region_rules_block_outside">Block calls outside allowed regions</string>

‎app/src/test/java/com/sysadmindoc/callshield/LocalizedSystemTextTest.kt‎

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import com.sysadmindoc.callshield.data.SmsContentAnalyzer
1313
import com.sysadmindoc.callshield.data.SpamRepository
1414
import com.sysadmindoc.callshield.data.checker.CheckContext
1515
import com.sysadmindoc.callshield.data.checker.EmergencyNumberFloorChecker
16+
import com.sysadmindoc.callshield.data.checker.RegulatoryAllowChecker
17+
import com.sysadmindoc.callshield.data.checker.RegulatoryPrefixChecker
1618
import com.sysadmindoc.callshield.data.checker.SmsContentChecker
1719
import com.sysadmindoc.callshield.data.checker.StirShakenChecker
1820
import com.sysadmindoc.callshield.data.checker.TimeBlockChecker
@@ -168,7 +170,7 @@ class LocalizedSystemTextTest {
168170
val description = requireNotNull(describeIdentityEvidence(context, identity))
169171

170172
assertEquals(
171-
"身份证据:PASSporT B 级认证元数据(运营商验证未通过);来电号码尚未分配;线路类型:付费电话;富来电数据元数据(并非垃圾来电判定)",
173+
"身份证据:PASSporT B 级认证元数据(运营商状态不是“通过”);来电号码尚未分配;线路类型:付费电话;富来电数据元数据(并非垃圾来电判定)",
172174
description,
173175
)
174176
for (english in listOf("Identity evidence", "metadata", "origin", "line type", "verdict", "carrier status")) {
@@ -177,27 +179,65 @@ class LocalizedSystemTextTest {
177179
}
178180

179181
@Test
180-
fun `the why-was-this-blocked panel has no English sentence in Chinese`() {
181-
// Three lowercase Latin words in a row is an English sentence; acronyms
182-
// and names the translation keeps (RCS, ML, NPA-NXX, PASSporT) are not.
183-
val englishRun = Regex("""\b[a-z]{2,}\s+[a-z]{2,}\s+[a-z]{2,}\b""")
182+
fun `the why-was-this-blocked panel has no English word in Chinese`() {
183+
// Names and acronyms the translation keeps. Any other Latin word, even a
184+
// single one like "range" or "spam", is English leaking into the panel.
185+
val kept = setOf("Android", "CallShield", "PASSporT", "VoIP")
186+
187+
fun englishWords(text: String) =
188+
Regex("[A-Za-z]+")
189+
.findAll(text)
190+
.map { it.value }
191+
.filterNot { it in kept || it.all(Char::isUpperCase) }
192+
.toList()
193+
194+
// A Chinese description renders the labeled bullets (note, range, rule)
195+
// without adding English of its own.
196+
val description = "测试"
184197
val explanations =
185-
BlockReasonCode.entries.map { BlockReasoning.explain(context, reasonCode = it, description = "", confidence = 50) } +
186-
BlockReasoning.explain(context, "category_policy:scam:silence:database", "", 100) +
187-
BlockReasoning.explain(context, "rcs_database", "", 100) +
198+
BlockReasonCode.entries.flatMap { code ->
199+
listOf("", description).map { BlockReasoning.explain(context, reasonCode = code, description = it, confidence = 50) }
200+
} +
201+
BlockReasoning.explain(context, "category_policy:scam:silence:database", description, 100) +
202+
BlockReasoning.explain(context, "rcs_database", description, 100) +
188203
BlockReasoning.explain(context, "", "", 0)
189204

190205
for (reasoning in explanations) {
191206
val text = (listOf(reasoning.headline) + reasoning.bullets).joinToString("\n")
192-
assertFalse(text, englishRun.containsMatchIn(text))
207+
assertEquals(text, emptyList<String>(), englishWords(text))
193208
assertTrue(text, text.any { it in '一'..'鿿' })
194209
}
210+
assertTrue(explanations.any { reasoning -> reasoning.bullets.any { it.contains(description) } })
195211
assertEquals(
196212
context.getString(R.string.reasoning_category_silenced, context.getString(R.string.call_category_scam)),
197213
explanations[explanations.size - 3].headline,
198214
)
215+
// The detector itself flags a lone English word.
216+
assertEquals(listOf("range"), englishWords("号段:range"))
199217
}
200218

219+
@Test
220+
fun `a regulatory range decision names the range in the app language`() =
221+
runBlocking {
222+
val block =
223+
requireNotNull(
224+
RegulatoryPrefixChecker().check(ctx(number = "+34400123456", prefs = preferencesOf(SpamRepository.KEY_REG_SPAIN_400 to true))),
225+
)
226+
val allow =
227+
requireNotNull(
228+
RegulatoryAllowChecker().check(
229+
ctx(number = "+911600123456", prefs = preferencesOf(SpamRepository.KEY_REG_INDIA_1600_ALLOW to true)),
230+
),
231+
)
232+
233+
assertEquals(context.getString(R.string.reg_prefix_spain_400_match), block.description)
234+
assertEquals(context.getString(R.string.reg_prefix_india_1600_allow_match), allow.description)
235+
assertFalse(block.description, block.description.contains("Spain"))
236+
assertFalse(allow.description, allow.description.contains("India"))
237+
val panel = BlockReasoning.explain(context, block.matchSource, block.description, block.confidence)
238+
assertTrue(panel.bullets.toString(), panel.bullets.any { it.contains(block.description) })
239+
}
240+
201241
@Test
202242
fun `unsafe links are named by kind, never by a feed's code`() {
203243
fun link(

‎scripts/translation_floors.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"description": "Minimum translated-string coverage per locale, in percent. check_translations.py fails when a shipped locale drops below its floor. --update-floors only raises a floor. Lowering one is a hand edit, made on purpose and explained in the commit message.",
33
"floors": {
4-
"values-zh-rCN": 78.9
4+
"values-zh-rCN": 81.3
55
}
66
}

0 commit comments

Comments
 (0)