address review: localize connection errors, tighten i18n guard, drop dead resources

- AddSourceViewModel resolves SubsonicException.uiMessage too, so malformed/
  HTTP/truncated responses show localized text instead of the raw parser
  message; add a regression test.
- StringResourcesTest now requires an 'other' quantity in every plural and
  asserts exact placeholder (index and conversion) equality, both of which
  could previously pass while crashing getQuantityString at runtime.
- Map the pre-enum song_downloads.error sentences back to their DownloadError
  so existing failed rows keep their specific message.
- Localize the media-session star/unstar button label.
- Remove 12 unreferenced resources (queue name, filter year, sync_failed, and
  the unused codec_* strings; codec labels stay in AudioEncoding).
- Make buildTree's fallback arguments required; only tests used the defaults.
This commit is contained in:
austinried
2026-10-04 00:30:07 +00:00
parent 3162ac07d5
commit 699a6e86f5
33 changed files with 59 additions and 55 deletions
@@ -24,6 +24,12 @@ class StringResourcesTest {
val default = parse(defaultFile)
assertTrue("default strings.xml should not be empty", default.isNotEmpty())
default.forEach { (name, resource) ->
if (resource.kind == "plurals") {
assertTrue("$name in values/strings.xml is missing the required 'other' quantity", "other" in resource.quantities)
}
}
localeFiles().forEach { file ->
val locale = parse(file)
locale.forEach { (name, resource) ->
@@ -35,12 +41,12 @@ class StringResourcesTest {
"$name has an invalid quantity in ${file.parentFile.name}",
resource.quantities.all { it in validQuantities },
)
assertTrue("$name in ${file.parentFile.name} is missing the required 'other' quantity", "other" in resource.quantities)
}
val baseArgs = placeholderIndices(base.text)
val localeArgs = placeholderIndices(resource.text)
assertTrue(
"$name in ${file.parentFile.name} uses placeholders $localeArgs not present in the default $baseArgs",
baseArgs.containsAll(localeArgs),
assertEquals(
"$name placeholders differ in ${file.parentFile.name}",
placeholders(base.text),
placeholders(resource.text),
)
}
}
@@ -84,6 +90,6 @@ class StringResourcesTest {
return (0 until items.length).joinToString(" ") { items.item(it).textContent }
}
private fun placeholderIndices(text: String): Set<Int> =
Regex("""%(\d+)\$""").findAll(text).mapTo(mutableSetOf()) { it.groupValues[1].toInt() }
private fun placeholders(text: String): Map<Int, Char> =
Regex("""%(\d+)\$([a-zA-Z])""").findAll(text).associate { it.groupValues[1].toInt() to it.groupValues[2][0] }
}
@@ -17,6 +17,8 @@ class DownloadTreeTest {
song("s3", "Three", "ar1", "Artist", "al2", "Second", size = 5),
song("s4", "Four", "ar2", "Other", "al3", "Third", size = 1),
),
"Unknown artist",
"Unknown album",
)
assertEquals(listOf("Artist", "Other"), tree.artists.map { it.name })
@@ -37,7 +39,7 @@ class DownloadTreeTest {
@Test
fun aSongWithNoArtistOrAlbumIsStillShown() {
val tree = buildTree(listOf(song("s1", "One", null, null, null, null, size = 3)))
val tree = buildTree(listOf(song("s1", "One", null, null, null, null, size = 3)), "Unknown artist", "Unknown album")
val artist = tree.artists.single()
assertEquals("Unknown artist", artist.name)
@@ -104,6 +104,20 @@ class AddSourceViewModelTest {
assertEquals("Failed: Wrong username", state.message)
}
@Test
fun aMalformedResponseIsReportedWithTheLocalizedMessage() {
val viewModel = viewModel()
viewModel.setAddress(server.url("/").toString())
viewModel.setUsername("u")
viewModel.setPassword("p")
server.enqueue(MockResponse().setBody("not an xml document"))
viewModel.testConnection()
await { !viewModel.state.value.busy && viewModel.state.value.isError }
assertEquals("Malformed response from the server", viewModel.state.value.message)
}
@Test
fun aSuccessfulPingClearsAnEarlierError() {
val viewModel = viewModel()