Localise the untitled-event placeholder (#321)

The instance and search mappers substituted a hardcoded German
"(Ohne Titel)" for an empty title, so EventInstance.title was never blank
and every ifBlank fallback in the views was dead code. Leave the title
blank and let the UI supply the translated string.

This also restores the drag identity guards in MonthDrag/TimelineDrag,
which check isNotBlank so two untitled events are not taken for the same
event (#253) — the injected literal made that check always pass.
This commit is contained in:
2026-09-23 21:44:01 +02:00
parent 66f6f10b27
commit 91406daf3e
6 changed files with 14 additions and 12 deletions
@@ -21,8 +21,9 @@ internal fun ColumnReader.toEventInstance(): EventInstance? {
return null return null
} }
val rawTitle = getString(InstanceProjection.IDX_TITLE) // Left blank when the event has no title: the placeholder is a display
val title = if (rawTitle.isNullOrEmpty()) Fallbacks.UNTITLED_EVENT else rawTitle // string, so it belongs to the UI layer where it can be localised (#321).
val title = getString(InstanceProjection.IDX_TITLE).orEmpty()
val color = if (isNull(InstanceProjection.IDX_EVENT_COLOR)) { val color = if (isNull(InstanceProjection.IDX_EVENT_COLOR)) {
getInt(InstanceProjection.IDX_CALENDAR_COLOR) getInt(InstanceProjection.IDX_CALENDAR_COLOR)
@@ -315,5 +315,4 @@ internal object ReminderProjection {
internal object Fallbacks { internal object Fallbacks {
const val UNNAMED_CALENDAR = "(Unbenannter Kalender)" const val UNNAMED_CALENDAR = "(Unbenannter Kalender)"
const val UNTITLED_EVENT = "(Ohne Titel)"
} }
@@ -21,8 +21,7 @@ internal fun ColumnReader.toSearchResult(): EventInstance? {
else -> dtStart + parseRfc2445DurationMillis(getString(SearchProjection.IDX_DURATION)) else -> dtStart + parseRfc2445DurationMillis(getString(SearchProjection.IDX_DURATION))
}.coerceAtLeast(dtStart) }.coerceAtLeast(dtStart)
val rawTitle = getString(SearchProjection.IDX_TITLE) val title = getString(SearchProjection.IDX_TITLE).orEmpty()
val title = if (rawTitle.isNullOrEmpty()) Fallbacks.UNTITLED_EVENT else rawTitle
val color = if (isNull(SearchProjection.IDX_EVENT_COLOR)) { val color = if (isNull(SearchProjection.IDX_EVENT_COLOR)) {
getInt(SearchProjection.IDX_CALENDAR_COLOR) getInt(SearchProjection.IDX_CALENDAR_COLOR)
} else { } else {
@@ -509,6 +509,9 @@ private fun SearchResultRow(
onLongClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null,
) { ) {
val event = hit.event val event = hit.event
// Spans are empty for a blank title — the query never matches the
// placeholder — so marking the substitute is safe.
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
val dark = isSystemInDarkTheme() val dark = isSystemInDarkTheme()
val soften = LocalSoftenColors.current val soften = LocalSoftenColors.current
// On a picked row the headline is already recoloured for the secondary // On a picked row the headline is already recoloured for the secondary
@@ -521,7 +524,7 @@ private fun SearchResultRow(
// Faded like a past event anywhere else in the app — search reaches back // Faded like a past event anywhere else in the app — search reaches back
// through the whole history. // through the whole history.
modifier = if (hit.isPast) modifier.alpha(EventDimAlpha) else modifier, modifier = if (hit.isPast) modifier.alpha(EventDimAlpha) else modifier,
title = declinedTitle(marked(event.title, hit.titleSpans, highlight), event.isDeclined), title = declinedTitle(marked(title, hit.titleSpans, highlight), event.isDeclined),
summary = searchSummary(hit, highlight), summary = searchSummary(hit, highlight),
position = position, position = position,
minHeight = 64.dp, minHeight = 64.dp,
@@ -61,15 +61,15 @@ class InstanceMapperTest {
} }
@Test @Test
fun `null title falls back to placeholder`() { fun `null title is left blank for the UI to fill in`() {
val inst = reader(title = null).toEventInstance() val inst = reader(title = null).toEventInstance()
assertThat(inst!!.title).isEqualTo(Fallbacks.UNTITLED_EVENT) assertThat(inst!!.title).isEmpty()
} }
@Test @Test
fun `empty title falls back to placeholder`() { fun `empty title is left blank for the UI to fill in`() {
val inst = reader(title = "").toEventInstance() val inst = reader(title = "").toEventInstance()
assertThat(inst!!.title).isEqualTo(Fallbacks.UNTITLED_EVENT) assertThat(inst!!.title).isEmpty()
} }
@Test @Test
@@ -242,8 +242,8 @@ class EventSearchTest {
} }
@Test @Test
fun `an untitled event's placeholder is not something the query can match`() { fun `an untitled event's blank title is not something the query can match`() {
val untitled = candidate("(Ohne Titel)", description = "Titel folgt noch").copy(title = null) val untitled = candidate("", description = "Titel folgt noch").copy(title = null)
val results = search(untitled, query = "titel") val results = search(untitled, query = "titel")