From 91406daf3e645ef84513b14695cc91f5d7b7ac9e Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 23 Sep 2026 21:44:01 +0200 Subject: [PATCH] Localise the untitled-event placeholder (#321) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../calendula/data/calendar/InstanceMapper.kt | 5 +++-- .../jeanlucmakiola/calendula/data/calendar/Projections.kt | 1 - .../calendula/data/calendar/SearchMapper.kt | 3 +-- .../de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt | 5 ++++- .../calendula/data/calendar/InstanceMapperTest.kt | 8 ++++---- .../de/jeanlucmakiola/calendula/domain/EventSearchTest.kt | 4 ++-- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/InstanceMapper.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/InstanceMapper.kt index 1fc7645..a1a9a18 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/InstanceMapper.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/InstanceMapper.kt @@ -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) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/Projections.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/Projections.kt index b535aee..c107d8f 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/Projections.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/Projections.kt @@ -315,5 +315,4 @@ internal object ReminderProjection { internal object Fallbacks { const val UNNAMED_CALENDAR = "(Unbenannter Kalender)" - const val UNTITLED_EVENT = "(Ohne Titel)" } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/SearchMapper.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/SearchMapper.kt index 23ee661..441f8c1 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/SearchMapper.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/SearchMapper.kt @@ -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 { diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt index e3333c8..2dde1ab 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt @@ -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, diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/InstanceMapperTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/InstanceMapperTest.kt index e52f384..2c58954 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/InstanceMapperTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/InstanceMapperTest.kt @@ -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 diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/domain/EventSearchTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/domain/EventSearchTest.kt index 710bab7..0ebae1f 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/domain/EventSearchTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/domain/EventSearchTest.kt @@ -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")