fix: re-escape the strings copied back to Android res, port the catalog filter test

aapt and compose-resources do not share escaping rules, so a key moved back to
res/values needs its apostrophes escaped again or the resource merge fails with
"Invalid unicode escape sequence in string". This is the inverse of what
tools/strings-migrate/fix_escapes.py does on the way out; a value Android
wrapped in quotes to protect whitespace, and a bare @string/ alias, are left
alone.

SettingsCatalogFilterTest used bare Ints as stand-in resource handles; it now
uses real catalog entries and still supplies their text through the injected
lookup, which is the point of the filter taking one.

:quartz, :commons, :commonsUI and :amethyst unit tests all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXvKXakvup4inNFfAhhr4L
This commit is contained in:
Claude
2026-09-22 19:34:04 +00:00
parent cb4b6b782f
commit 67ed6379ff
10 changed files with 73 additions and 56 deletions
@@ -169,7 +169,7 @@
<string name="always_on_notif_connecting">Connexion aux relais de boîte de réception…</string>
<string name="always_on_notif_title">Notifications Amethyst actives</string>
<string name="call_ongoing">Appels</string>
<string name="call_ongoing_description">Notification d'appel en cours</string>
<string name="call_ongoing_description">Notification d\'appel en cours</string>
<string name="call_with">Appel avec %1$s</string>
<string name="call_hangup">Raccrocher</string>
<string name="reaction">Réaction</string>
@@ -178,7 +178,7 @@
<string name="voice_reply">Réponse vocale</string>
<string name="private_message">Message privé</string>
<string name="nest_notification_broadcasting">Nest — En direct</string>
<string name="nest_notification_channel_description">Maintient l'audio actif pendant qu'une salle est ouverte.</string>
<string name="nest_notification_channel_description">Maintient l\'audio actif pendant qu\'une salle est ouverte.</string>
<string name="nest_notification_listening">Nest connecté</string>
<string name="nest_notification_stop">Arrêter</string>
<string name="nest_notification_text">Appuyer pour revenir.</string>
@@ -185,7 +185,7 @@
<string name="always_on_notif_connecting">Connexion aux relais de boîte de réception…</string>
<string name="always_on_notif_title">Notifications Amethyst actives</string>
<string name="call_ongoing">Appels</string>
<string name="call_ongoing_description">Notification d'appel en cours</string>
<string name="call_ongoing_description">Notification d\'appel en cours</string>
<string name="call_with">Appel avec %1$s</string>
<string name="call_hangup">Raccrocher</string>
<string name="pow_time_left_soon">à tout moment maintenant</string>
@@ -205,7 +205,7 @@
<string name="pow_kind_report">Signaler</string>
<string name="private_message">Message privé</string>
<string name="nest_notification_broadcasting">Nest — En direct</string>
<string name="nest_notification_channel_description">Maintient l'audio actif pendant qu'une salle est ouverte.</string>
<string name="nest_notification_channel_description">Maintient l\'audio actif pendant qu\'une salle est ouverte.</string>
<string name="nest_notification_listening">Nest connecté</string>
<string name="nest_notification_stop">Arrêter</string>
<string name="nest_notification_text">Appuyer pour revenir.</string>
+2 -2
View File
@@ -2316,7 +2316,7 @@
<string name="always_on_notif_connecting">Connexion aux relais de boîte de réception…</string>
<string name="always_on_notif_title">Notifications Amethyst actives</string>
<string name="call_ongoing">Appels</string>
<string name="call_ongoing_description">Notification d'appel en cours</string>
<string name="call_ongoing_description">Notification d\'appel en cours</string>
<string name="call_with">Appel avec %1$s</string>
<string name="call_hangup">Raccrocher</string>
<string name="reaction">Réaction</string>
@@ -2327,7 +2327,7 @@
<string name="private_message">Message privé</string>
<string name="nest_notification_broadcasting">Nest — En direct</string>
<string name="nest_notification_channel">Nests</string>
<string name="nest_notification_channel_description">Maintient l'audio actif pendant qu'une salle est ouverte.</string>
<string name="nest_notification_channel_description">Maintient l\'audio actif pendant qu\'une salle est ouverte.</string>
<string name="nest_notification_listening">Nest connecté</string>
<string name="nest_notification_stop">Arrêter</string>
<string name="nest_notification_text">Appuyer pour revenir.</string>
@@ -176,7 +176,7 @@
<string name="voice_post">Post vocale</string>
<string name="voice_reply">Risposta vocale</string>
<string name="private_message">Messaggio privato</string>
<string name="nest_notification_channel_description">Mantiene l'audio attivo mentre una stanza è aperta.</string>
<string name="nest_notification_channel_description">Mantiene l\'audio attivo mentre una stanza è aperta.</string>
<string name="nest_notification_listening">Nest connesso</string>
<string name="nest_notification_stop">Ferma</string>
<string name="nest_notification_text">Tocca per tornare.</string>
@@ -195,7 +195,7 @@
<string name="napplet_cap_theme">Tema</string>
<string name="napplet_cap_notify">Notifiche</string>
<string name="napplet_cap_inc">Messaggistica</string>
<string name="muted_button">Mutato. Clicca per riattivare l'audio</string>
<string name="muted_button">Mutato. Clicca per riattivare l\'audio</string>
<string name="mute_button">Audio acceso. Clicca per disattivarlo</string>
<string name="play">Riproduci</string>
<string name="pause">Pausa</string>
@@ -158,16 +158,16 @@
because android:label on the manifest entry can only reference @string/. -->
<!-- My Fitness: the signed-in user's own training summary. Android resource because
NavBarItemDef labels are R.string ids. -->
<string name="always_on_notif_channel_description">Gerçek zamanlı bildirimler için gelen kutusu relay'lerinize bağlantıları etkin tutar</string>
<string name="always_on_notif_channel_description">Gerçek zamanlı bildirimler için gelen kutusu relay\'lerinize bağlantıları etkin tutar</string>
<plurals name="always_on_notif_connected">
<item quantity="one">%1$d gelen kutusu relay'ine bağlandı</item>
<item quantity="other">%1$d gelen kutusu relay'ine bağlandı</item>
<item quantity="one">%1$d gelen kutusu relay\'ine bağlandı</item>
<item quantity="other">%1$d gelen kutusu relay\'ine bağlandı</item>
</plurals>
<plurals name="always_on_notif_connected_foreground">
<item quantity="one">%1$d relay'e bağlandı</item>
<item quantity="other">%1$d relay'e bağlandı</item>
<item quantity="one">%1$d relay\'e bağlandı</item>
<item quantity="other">%1$d relay\'e bağlandı</item>
</plurals>
<string name="always_on_notif_connecting">Gelen kutusu relay'lerine bağlanılıyor…</string>
<string name="always_on_notif_connecting">Gelen kutusu relay\'lerine bağlanılıyor…</string>
<string name="always_on_notif_title">Amethyst Bildirimleri Etkin</string>
<string name="call_ongoing">Aramalar</string>
<string name="call_ongoing_description">Devam eden arama bildirimi</string>
@@ -191,7 +191,7 @@
<string name="call_unmute">Sesi Aç</string>
<string name="napplet_cap_identity">Kimlik</string>
<string name="napplet_cap_keys">Klavye eylemleri</string>
<string name="napplet_cap_relay">relay'ler</string>
<string name="napplet_cap_relay">relay\'ler</string>
<string name="napplet_cap_storage">Depolama</string>
<string name="napplet_cap_value">Ödemeler</string>
<string name="napplet_cap_resource">Ağ</string>
+7 -7
View File
@@ -2881,16 +2881,16 @@
<string name="always_on_notif_channel_description">Gerçek zamanlı bildirimler için gelen kutusu relay'lerinize bağlantıları etkin tutar</string>
<string name="always_on_notif_channel_description">Gerçek zamanlı bildirimler için gelen kutusu relay\'lerinize bağlantıları etkin tutar</string>
<plurals name="always_on_notif_connected">
<item quantity="one">%1$d gelen kutusu relay'ine bağlandı</item>
<item quantity="other">%1$d gelen kutusu relay'ine bağlandı</item>
<item quantity="one">%1$d gelen kutusu relay\'ine bağlandı</item>
<item quantity="other">%1$d gelen kutusu relay\'ine bağlandı</item>
</plurals>
<plurals name="always_on_notif_connected_foreground">
<item quantity="one">%1$d relay'e bağlandı</item>
<item quantity="other">%1$d relay'e bağlandı</item>
<item quantity="one">%1$d relay\'e bağlandı</item>
<item quantity="other">%1$d relay\'e bağlandı</item>
</plurals>
<string name="always_on_notif_connecting">Gelen kutusu relay'lerine bağlanılıyor…</string>
<string name="always_on_notif_connecting">Gelen kutusu relay\'lerine bağlanılıyor…</string>
<string name="always_on_notif_title">Amethyst Bildirimleri Etkin</string>
<string name="call_ongoing">Aramalar</string>
<string name="call_ongoing_description">Devam eden arama bildirimi</string>
@@ -2915,7 +2915,7 @@
<string name="call_unmute">Sesi Aç</string>
<string name="napplet_cap_identity">Kimlik</string>
<string name="napplet_cap_keys">Klavye eylemleri</string>
<string name="napplet_cap_relay">relay'ler</string>
<string name="napplet_cap_relay">relay\'ler</string>
<string name="napplet_cap_storage">Depolama</string>
<string name="napplet_cap_value">Ödemeler</string>
<string name="napplet_cap_resource">Ağ</string>
@@ -158,7 +158,7 @@
because android:label on the manifest entry can only reference @string/. -->
<!-- My Fitness: the signed-in user's own training summary. Android resource because
NavBarItemDef labels are R.string ids. -->
<string name="always_on_notif_channel_description">Підтримує активні з'єднання з вашими inbox relay для сповіщень у реальному часі</string>
<string name="always_on_notif_channel_description">Підтримує активні з\'єднання з вашими inbox relay для сповіщень у реальному часі</string>
<plurals name="always_on_notif_connected">
<item quantity="one">Підключено до %1$d inbox relay</item>
<item quantity="few">Підключено до %1$d inbox relay</item>
+1 -1
View File
@@ -2640,7 +2640,7 @@
<string name="always_on_notif_channel_description">Підтримує активні з'єднання з вашими inbox relay для сповіщень у реальному часі</string>
<string name="always_on_notif_channel_description">Підтримує активні з\'єднання з вашими inbox relay для сповіщень у реальному часі</string>
<plurals name="always_on_notif_connected">
<item quantity="one">Підключено до %1$d inbox relay</item>
<item quantity="few">Підключено до %1$d inbox relay</item>
+12 -12
View File
@@ -160,18 +160,18 @@
NavBarItemDef labels are R.string ids. -->
<string name="always_on_notif_channel_description">Real vaqtli bildirishnomalar uchun kirish relaylaringizga ulanishni faol saqlaydi</string>
<plurals name="always_on_notif_connected">
<item quantity="one">%1$d kirish relay'iga ulangan</item>
<item quantity="other">%1$d kirish relay'lariga ulangan</item>
<item quantity="one">%1$d kirish relay\'iga ulangan</item>
<item quantity="other">%1$d kirish relay\'lariga ulangan</item>
</plurals>
<plurals name="always_on_notif_connected_foreground">
<item quantity="one">%1$d relay'ga ulangan</item>
<item quantity="other">%1$d relay'larga ulangan</item>
<item quantity="one">%1$d relay\'ga ulangan</item>
<item quantity="other">%1$d relay\'larga ulangan</item>
</plurals>
<string name="always_on_notif_connecting">Kirish relaylariga ulanilmoqda…</string>
<string name="always_on_notif_title">Amethyst Bildirishnomalari Faol</string>
<string name="call_ongoing">Qo'ng'iroqlar</string>
<string name="call_ongoing_description">Davom etayotgan qo'ng'iroq bildirishномаsi</string>
<string name="call_with">%1$s bilan qo'ng'iroq</string>
<string name="call_ongoing">Qo\'ng\'iroqlar</string>
<string name="call_ongoing_description">Davom etayotgan qo\'ng\'iroq bildirishномаsi</string>
<string name="call_with">%1$s bilan qo\'ng\'iroq</string>
<string name="call_hangup">Tugatish</string>
<string name="reaction">Reaksiya</string>
<string name="boost">Targʻib qilish</string>
@@ -180,20 +180,20 @@
<string name="private_message">Shaxsiy Xabar</string>
<string name="nest_notification_broadcasting">Nest — Jonli</string>
<string name="nest_notification_channel">Nestlar</string>
<string name="nest_notification_channel_description">Xona ochiq bo'lganda audio ijrosini davom ettiradi.</string>
<string name="nest_notification_channel_description">Xona ochiq bo\'lganda audio ijrosini davom ettiradi.</string>
<string name="nest_notification_listening">Nest ulandi</string>
<string name="nest_notification_stop">To'xtatish</string>
<string name="nest_notification_stop">To\'xtatish</string>
<string name="nest_notification_text">Qaytish uchun bosing.</string>
<string name="nest_leave">Chiqish</string>
<string name="nest_mute">Ovozni o'chirish</string>
<string name="nest_mute">Ovozni o\'chirish</string>
<string name="nest_unmute">Ovozni yoqish</string>
<string name="call_mute">Ovozni o'chirish</string>
<string name="call_mute">Ovozni o\'chirish</string>
<string name="call_unmute">Ovozni yoqish</string>
<string name="napplet_cap_identity">Shaxsiyat</string>
<string name="napplet_cap_keys">Klaviatura amallari</string>
<string name="napplet_cap_relay">Relaylar</string>
<string name="napplet_cap_storage">Saqlash</string>
<string name="napplet_cap_value">To'lovlar</string>
<string name="napplet_cap_value">To\'lovlar</string>
<string name="napplet_cap_resource">Tarmoq</string>
<string name="napplet_cap_upload">Yuklamalar</string>
<string name="napplet_cap_theme">Mavzu</string>
@@ -20,25 +20,42 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.account_settings
import com.vitorpamplona.amethyst.commons.resources.backup_keys
import com.vitorpamplona.amethyst.commons.resources.danger_zone
import com.vitorpamplona.amethyst.commons.resources.relays
import com.vitorpamplona.amethyst.commons.resources.settings_section_appearance
import com.vitorpamplona.amethyst.commons.resources.theme
import org.jetbrains.compose.resources.StringResource
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class SettingsCatalogFilterTest {
// Real catalog entries stand in for the rows, but their text comes from this map:
// filterSettings takes its lookup as a parameter precisely so the filter itself
// stays Compose-free, and resolving a StringResource for real would need a runtime.
private val accountCategory = Res.string.account_settings
private val dangerCategory = Res.string.danger_zone
private val relayEntry = Res.string.relays
private val uiEntry = Res.string.settings_section_appearance
private val backupEntry = Res.string.backup_keys
private val uiKeywords = Res.string.theme
private val strings =
mapOf(
100 to "Account Settings",
200 to "Danger Zone",
1 to "Relay Setup",
2 to "UI Preferences",
3 to "Backup Keys",
20 to "dark mode, theme, font size",
accountCategory to "Account Settings",
dangerCategory to "Danger Zone",
relayEntry to "Relay Setup",
uiEntry to "UI Preferences",
backupEntry to "Backup Keys",
uiKeywords to "dark mode, theme, font size",
)
private fun entry(
titleRes: StringResource,
keywordsRes: Int? = null,
keywordsRes: StringResource? = null,
isDanger: Boolean = false,
) = SettingsEntry(
titleRes = titleRes,
@@ -51,17 +68,17 @@ class SettingsCatalogFilterTest {
private val catalog =
listOf(
SettingsCategory(
titleRes = 100,
titleRes = accountCategory,
entries =
listOf(
entry(1),
entry(2, keywordsRes = 20),
entry(relayEntry),
entry(uiEntry, keywordsRes = uiKeywords),
),
),
SettingsCategory(
titleRes = 200,
titleRes = dangerCategory,
isDanger = true,
entries = listOf(entry(3, isDanger = true)),
entries = listOf(entry(backupEntry, isDanger = true)),
),
)
@@ -89,30 +106,30 @@ class SettingsCatalogFilterTest {
fun titleMatchIsCaseInsensitive() {
val result = run("relay")
assertEquals(1, result.size)
assertEquals(100, result[0].titleRes)
assertEquals(accountCategory, result[0].titleRes)
assertEquals(1, result[0].entries.size)
assertEquals(1, result[0].entries[0].titleRes)
assertEquals(relayEntry, result[0].entries[0].titleRes)
}
@Test
fun keywordMatchSurfacesEntryWhoseTitleDoesNotMatch() {
val result = run("dark mode")
assertEquals(1, result.size)
assertEquals(2, result[0].entries[0].titleRes) // UI Preferences, matched via keywords
assertEquals(uiEntry, result[0].entries[0].titleRes) // UI Preferences, matched via keywords
}
@Test
fun categoryTitleMatchSurfacesWholeCategory() {
val result = run("account")
assertEquals(1, result.size)
assertEquals(100, result[0].titleRes)
assertEquals(accountCategory, result[0].titleRes)
assertEquals(2, result[0].entries.size) // both rows shown because the category name matched
}
@Test
fun categoryWithNoMatchesIsDropped() {
val result = run("relay")
assertTrue(result.none { it.titleRes == 200 })
assertTrue(result.none { it.titleRes == dangerCategory })
}
@Test
@@ -131,8 +148,8 @@ class SettingsCatalogFilterTest {
@Test
fun prefixOfAWordMatches() {
// "rel" is a prefix of "Relay" (title); "the" is a prefix of "theme" (keyword).
assertEquals(1, run("rel")[0].entries[0].titleRes)
assertEquals(2, run("the")[0].entries[0].titleRes)
assertEquals(relayEntry, run("rel")[0].entries[0].titleRes)
assertEquals(uiEntry, run("the")[0].entries[0].titleRes)
}
@Test
@@ -143,7 +160,7 @@ class SettingsCatalogFilterTest {
@Test
fun everyQueryTermMustPrefixSomeWord() {
assertEquals(2, run("dark size")[0].entries[0].titleRes) // both terms hit UI Preferences
assertEquals(uiEntry, run("dark size")[0].entries[0].titleRes) // both terms hit UI Preferences
assertTrue(run("dark zzz").isEmpty()) // second term matches nothing
}
}