Skip to content

Commit f1dab6e

Browse files
adsamcikCopilot
andcommitted
fix(dashboard): humanize achievement-name fallback when string resource is missing
QC sweep caught the Latest Achievement card showing raw resource keys to users: 'achievement_sessions_total_1_title' instead of 'Sessions Total 1'. The catalog auto-generates ~200 achievement ids in :stats-api/AchievementCatalog and references string resources by name (e.g. 'achievement__title'), but the localized titles are not yet authored. resolveStringResource used to leak the raw key as the visible label. Smallest improvement that helps users: when getIdentifier returns 0, strip the 'achievement_' prefix and '_title' / '_desc' / '_description' suffix, then title-case the remaining tokens. Users now see 'Sessions Total 1' / 'Total Distance 5000' / 'Active Days 7' instead of the resource key. Proper localized titles remain a separate follow-up. Tests: 10 cases covering prefix/suffix stripping, single-word ids, consecutive underscores, suffix-in-the-middle, empty input, and the degenerate 'achievement_title' case. Verified on emulator after triggering an end-of-session AchievementWorker: card text changed from 'achievement_sessions_total_1_title' to 'Sessions Total 1'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 641dfad commit f1dab6e

2 files changed

Lines changed: 88 additions & 1 deletion

File tree

dashboard/src/main/java/com/adsamcik/tracker/dashboard/ui/compose/cards/IdleContent.kt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import androidx.compose.ui.Modifier
2424
import androidx.compose.ui.platform.LocalConfiguration
2525
import androidx.compose.ui.platform.LocalContext
2626
import androidx.compose.ui.res.stringResource
27+
import androidx.annotation.VisibleForTesting
2728
import androidx.compose.ui.unit.Dp
2829
import androidx.compose.ui.unit.dp
2930
import com.adsamcik.tracker.dashboard.R
@@ -270,6 +271,29 @@ private fun LatestAchievementCard(state: DashboardUiState, modifier: Modifier =
270271
private fun resolveStringResource(name: String): String {
271272
val context = LocalContext.current
272273
val resId = remember(name, context) { context.resources.getIdentifier(name, "string", context.packageName) }
273-
return if (resId != 0) stringResource(resId) else name
274+
return if (resId != 0) {
275+
stringResource(resId)
276+
} else {
277+
// No string resource defined for this achievement id yet (the catalog
278+
// auto-generates ~200 ids; localized titles are still being authored).
279+
// Show a readable humanized fallback instead of leaking the raw key
280+
// like "achievement_sessions_total_1_title" to the user.
281+
humanizeResourceKey(name)
282+
}
283+
}
284+
285+
@VisibleForTesting
286+
internal fun humanizeResourceKey(name: String): String {
287+
val core = name
288+
.removePrefix("achievement_")
289+
.removeSuffix("_title")
290+
.removeSuffix("_desc")
291+
.removeSuffix("_description")
292+
if (core.isBlank()) return name
293+
return core.split('_')
294+
.filter { it.isNotEmpty() }
295+
.joinToString(" ") { token ->
296+
token.replaceFirstChar { ch -> if (ch.isLowerCase()) ch.titlecase() else ch.toString() }
297+
}
274298
}
275299

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.adsamcik.tracker.dashboard.ui.compose.cards
2+
3+
import io.kotest.matchers.shouldBe
4+
import org.junit.jupiter.api.DisplayName
5+
import org.junit.jupiter.api.Test
6+
7+
@DisplayName("humanizeResourceKey - achievement title fallback when no string resource exists")
8+
class HumanizeResourceKeyTest {
9+
10+
@Test
11+
fun `strips achievement_ prefix and _title suffix and title-cases tokens`() {
12+
humanizeResourceKey("achievement_sessions_total_1_title") shouldBe "Sessions Total 1"
13+
}
14+
15+
@Test
16+
fun `also handles _desc suffix`() {
17+
humanizeResourceKey("achievement_total_distance_5000_desc") shouldBe "Total Distance 5000"
18+
}
19+
20+
@Test
21+
fun `also handles _description suffix`() {
22+
humanizeResourceKey("achievement_active_days_7_description") shouldBe "Active Days 7"
23+
}
24+
25+
@Test
26+
fun `unrelated keys still get title-cased (no false stripping)`() {
27+
humanizeResourceKey("some_other_key") shouldBe "Some Other Key"
28+
}
29+
30+
@Test
31+
fun `keys without achievement prefix still get title-cased`() {
32+
humanizeResourceKey("max_session_duration_3600000_title") shouldBe "Max Session Duration 3600000"
33+
}
34+
35+
@Test
36+
fun `empty string returns empty string`() {
37+
humanizeResourceKey("") shouldBe ""
38+
}
39+
40+
@Test
41+
fun `degenerate key with only prefix gets title-cased remainder`() {
42+
// "achievement_title" -> strip "achievement_" -> "title" -> "Title".
43+
// Suffix-strip only matches "_title" (with separator), so "title"
44+
// alone survives the strip pass and becomes a single capitalized word.
45+
humanizeResourceKey("achievement_title") shouldBe "Title"
46+
}
47+
48+
@Test
49+
fun `consecutive underscores are collapsed`() {
50+
humanizeResourceKey("achievement_a__b_title") shouldBe "A B"
51+
}
52+
53+
@Test
54+
fun `single-word id is title-cased`() {
55+
humanizeResourceKey("achievement_walker_title") shouldBe "Walker"
56+
}
57+
58+
@Test
59+
fun `does not double-strip suffix that appears in the middle`() {
60+
// "title" inside the body should not be touched -- only suffix.
61+
humanizeResourceKey("achievement_title_holder_title") shouldBe "Title Holder"
62+
}
63+
}

0 commit comments

Comments
 (0)