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
}
val rawTitle = getString(InstanceProjection.IDX_TITLE)
val title = if (rawTitle.isNullOrEmpty()) Fallbacks.UNTITLED_EVENT else rawTitle
// Left blank when the event has no title: the placeholder is a display
// 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)) {
getInt(InstanceProjection.IDX_CALENDAR_COLOR)
@@ -315,5 +315,4 @@ internal object ReminderProjection {
internal object Fallbacks {
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))
}.coerceAtLeast(dtStart)
val rawTitle = getString(SearchProjection.IDX_TITLE)
val title = if (rawTitle.isNullOrEmpty()) Fallbacks.UNTITLED_EVENT else rawTitle
val title = getString(SearchProjection.IDX_TITLE).orEmpty()
val color = if (isNull(SearchProjection.IDX_EVENT_COLOR)) {
getInt(SearchProjection.IDX_CALENDAR_COLOR)
} else {
@@ -509,6 +509,9 @@ private fun SearchResultRow(
onLongClick: (() -> Unit)? = null,
) {
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 soften = LocalSoftenColors.current
// 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
// through the whole history.
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),
position = position,
minHeight = 64.dp,
@@ -61,15 +61,15 @@ class InstanceMapperTest {
}
@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()
assertThat(inst!!.title).isEqualTo(Fallbacks.UNTITLED_EVENT)
assertThat(inst!!.title).isEmpty()
}
@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()
assertThat(inst!!.title).isEqualTo(Fallbacks.UNTITLED_EVENT)
assertThat(inst!!.title).isEmpty()
}
@Test
@@ -242,8 +242,8 @@ class EventSearchTest {
}
@Test
fun `an untitled event's placeholder is not something the query can match`() {
val untitled = candidate("(Ohne Titel)", description = "Titel folgt noch").copy(title = null)
fun `an untitled event's blank title is not something the query can match`() {
val untitled = candidate("", description = "Titel folgt noch").copy(title = null)
val results = search(untitled, query = "titel")