Localise the unnamed-calendar placeholder (#329)

CalendarMapper substituted a hardcoded German "(Unbenannter Kalender)"
for a null CALENDAR_DISPLAY_NAME, so it showed in every locale wherever
calendars are listed — and the calendar editor prefills from that name,
so saving wrote the literal into the provider for real. Leave the name
blank and add a calendar_unnamed string the views substitute.

The write paths now store the trimmed name as given instead of inventing
one: a stored placeholder would freeze the creation-time language into
the provider, and the editor already refuses a blank name.
This commit is contained in:
2026-09-23 21:55:57 +02:00
parent 91406daf3e
commit 2f497b2b95
15 changed files with 38 additions and 31 deletions
@@ -376,7 +376,7 @@ class AndroidCalendarDataSource @Inject constructor(
.build() .build()
override fun createLocalCalendar(displayName: String, color: Int, description: String?): Long { override fun createLocalCalendar(displayName: String, color: Int, description: String?): Long {
val name = displayName.trim().ifEmpty { Fallbacks.UNNAMED_CALENDAR } val name = displayName.trim()
val values = ContentValues().apply { val values = ContentValues().apply {
put(CalendarContract.Calendars.ACCOUNT_NAME, LOCAL_ACCOUNT_NAME) put(CalendarContract.Calendars.ACCOUNT_NAME, LOCAL_ACCOUNT_NAME)
put(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL) put(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
@@ -400,7 +400,7 @@ class AndroidCalendarDataSource @Inject constructor(
} }
override fun updateCalendar(id: Long, displayName: String, color: Int, description: String?) { override fun updateCalendar(id: Long, displayName: String, color: Int, description: String?) {
val name = displayName.trim().ifEmpty { Fallbacks.UNNAMED_CALENDAR } val name = displayName.trim()
val values = ContentValues().apply { val values = ContentValues().apply {
put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, name) put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, name)
put(CalendarContract.Calendars.NAME, name) put(CalendarContract.Calendars.NAME, name)
@@ -464,7 +464,7 @@ class AndroidCalendarDataSource @Inject constructor(
PackageManager.PERMISSION_GRANTED PackageManager.PERMISSION_GRANTED
override fun createManagedCalendar(displayName: String, color: Int, type: SpecialDateType): Long { override fun createManagedCalendar(displayName: String, color: Int, type: SpecialDateType): Long {
val name = displayName.trim().ifEmpty { Fallbacks.UNNAMED_CALENDAR } val name = displayName.trim()
val values = ContentValues().apply { val values = ContentValues().apply {
put(CalendarContract.Calendars.ACCOUNT_NAME, LOCAL_ACCOUNT_NAME) put(CalendarContract.Calendars.ACCOUNT_NAME, LOCAL_ACCOUNT_NAME)
put(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL) put(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
@@ -8,8 +8,9 @@ internal fun ColumnReader.toCalendarSource(): CalendarSource {
val isLocal = accountType == CalendarContract.ACCOUNT_TYPE_LOCAL val isLocal = accountType == CalendarContract.ACCOUNT_TYPE_LOCAL
return CalendarSource( return CalendarSource(
id = getLong(CalendarProjection.IDX_ID), id = getLong(CalendarProjection.IDX_ID),
displayName = getString(CalendarProjection.IDX_DISPLAY_NAME) // Blank when the provider row has no name: the placeholder is a display
?: Fallbacks.UNNAMED_CALENDAR, // string and belongs to the UI, where it can be localised (#329).
displayName = getString(CalendarProjection.IDX_DISPLAY_NAME).orEmpty(),
accountName = getString(CalendarProjection.IDX_ACCOUNT_NAME).orEmpty(), accountName = getString(CalendarProjection.IDX_ACCOUNT_NAME).orEmpty(),
accountType = accountType, accountType = accountType,
color = getInt(CalendarProjection.IDX_COLOR), color = getInt(CalendarProjection.IDX_COLOR),
@@ -312,7 +312,3 @@ internal object ReminderProjection {
const val IDX_MINUTES = 0 const val IDX_MINUTES = 0
const val IDX_METHOD = 1 const val IDX_METHOD = 1
} }
internal object Fallbacks {
const val UNNAMED_CALENDAR = "(Unbenannter Kalender)"
}
@@ -312,7 +312,7 @@ private fun ExportCalendarPicker(
calendars.forEachIndexed { index, calendar -> calendars.forEachIndexed { index, calendar ->
val isSelected = calendar.id in selected val isSelected = calendar.id in selected
GroupedRow( GroupedRow(
title = calendar.displayName, title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
summary = calendar.description, summary = calendar.description,
position = positionOf(index, calendars.size), position = positionOf(index, calendars.size),
leading = { CalendarColorChip(calendar.color) }, leading = { CalendarColorChip(calendar.color) },
@@ -246,7 +246,7 @@ private fun CalendarsList(
local.forEachIndexed { index, calendar -> local.forEachIndexed { index, calendar ->
val disabled = !calendar.isVisibleInSystem val disabled = !calendar.isVisibleInSystem
GroupedRow( GroupedRow(
title = calendar.displayName, title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
summary = calendarRowSummary(calendar), summary = calendarRowSummary(calendar),
position = if (index == local.lastIndex) Position.Bottom else Position.Middle, position = if (index == local.lastIndex) Position.Bottom else Position.Middle,
container = MaterialTheme.colorScheme.surfaceContainerHighest, container = MaterialTheme.colorScheme.surfaceContainerHighest,
@@ -254,7 +254,7 @@ private fun CalendarsList(
leading = { CalendarColorChip(calendar.color, dimIf(disabled)) }, leading = { CalendarColorChip(calendar.color, dimIf(disabled)) },
trailing = { trailing = {
EnableSwitch( EnableSwitch(
calendarName = calendar.displayName, calendarName = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
enabled = !disabled, enabled = !disabled,
onToggle = { enabled -> onSetVisible(calendar.id, enabled) }, onToggle = { enabled -> onSetVisible(calendar.id, enabled) },
) )
@@ -331,7 +331,7 @@ private fun CalendarsList(
ordered.forEachIndexed { index, calendar -> ordered.forEachIndexed { index, calendar ->
val disabled = !calendar.isVisibleInSystem || calendar.isNotSynced val disabled = !calendar.isVisibleInSystem || calendar.isNotSynced
GroupedRow( GroupedRow(
title = calendar.displayName, title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
summary = calendarRowSummary(calendar), summary = calendarRowSummary(calendar),
position = if (index == ordered.lastIndex) Position.Bottom else Position.Middle, position = if (index == ordered.lastIndex) Position.Bottom else Position.Middle,
container = MaterialTheme.colorScheme.surfaceContainerHighest, container = MaterialTheme.colorScheme.surfaceContainerHighest,
@@ -340,7 +340,7 @@ private fun CalendarsList(
trailing = if (calendar.hasVisibilitySwitch) { trailing = if (calendar.hasVisibilitySwitch) {
{ {
EnableSwitch( EnableSwitch(
calendarName = calendar.displayName, calendarName = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
enabled = calendar.isVisibleInSystem, enabled = calendar.isVisibleInSystem,
onToggle = { enabled -> onToggle = { enabled ->
onSetVisible(calendar.id, enabled) onSetVisible(calendar.id, enabled)
@@ -56,12 +56,14 @@ fun List<CalendarSource>.groupByAccount(): List<CalendarAccountGroup> {
* account comes from when another account shares the name (#77). * account comes from when another account shares the name (#77).
*/ */
@Composable @Composable
fun accountGroupTitle(group: CalendarAccountGroup): String = fun accountGroupTitle(group: CalendarAccountGroup): String {
if (!group.ambiguous) { val label = group.label.ifBlank { stringResource(R.string.calendar_unnamed) }
group.label return if (!group.ambiguous) {
label
} else { } else {
stringResource(R.string.calendars_account_from_source, group.label, sourceAppName(group.accountType)) stringResource(R.string.calendars_account_from_source, label, sourceAppName(group.accountType))
} }
}
/** /**
* The human name of the app backing [accountType], falling back to the raw * The human name of the app backing [accountType], falling back to the raw
@@ -116,7 +116,7 @@ private fun CalendarPickerGroup(
calendars.forEachIndexed { index, calendar -> calendars.forEachIndexed { index, calendar ->
val isSelected = calendar.id == selectedId val isSelected = calendar.id == selectedId
GroupedRow( GroupedRow(
title = calendar.displayName, title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
position = if (index == calendars.lastIndex) Position.Bottom else Position.Middle, position = if (index == calendars.lastIndex) Position.Bottom else Position.Middle,
selected = isSelected, selected = isSelected,
leading = { CalendarColorChip(calendar.color) }, leading = { CalendarColorChip(calendar.color) },
@@ -527,7 +527,8 @@ private fun EventDetailContent(
iconContentDescription = stringResource(R.string.event_detail_calendar), iconContentDescription = stringResource(R.string.event_detail_calendar),
) { ) {
Text( Text(
text = state.calendarName ?: stringResource(R.string.event_detail_calendar_unknown), text = state.calendarName?.ifBlank { stringResource(R.string.calendar_unnamed) }
?: stringResource(R.string.event_detail_calendar_unknown),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
) )
} }
@@ -620,7 +620,9 @@ private fun EventEditContent(
Text( Text(
text = stringResource( text = stringResource(
R.string.event_edit_managed_hint, R.string.event_edit_managed_hint,
selectedCalendar?.displayName.orEmpty(), selectedCalendar?.displayName
?.ifBlank { stringResource(R.string.calendar_unnamed) }
.orEmpty(),
), ),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -719,6 +721,7 @@ private fun EventEditContent(
) { ) {
Text( Text(
text = selectedCalendar?.displayName text = selectedCalendar?.displayName
?.ifBlank { stringResource(R.string.calendar_unnamed) }
?: stringResource(R.string.event_edit_error_no_calendar), ?: stringResource(R.string.event_edit_error_no_calendar),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = if (selectedCalendar == null) { color = if (selectedCalendar == null) {
@@ -62,15 +62,16 @@ private fun FilterList(
) { ) {
Column(modifier = modifier.fillMaxWidth()) { Column(modifier = modifier.fillMaxWidth()) {
groups.forEach { group -> groups.forEach { group ->
val account = group.account.ifBlank { stringResource(R.string.calendar_unnamed) }
Text( Text(
text = if (group.ambiguous) { text = if (group.ambiguous) {
stringResource( stringResource(
R.string.calendars_account_from_source, R.string.calendars_account_from_source,
group.account, account,
sourceAppName(group.accountType), sourceAppName(group.accountType),
) )
} else { } else {
group.account account
}, },
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -83,7 +84,7 @@ private fun FilterList(
) )
group.calendars.forEachIndexed { index, cal -> group.calendars.forEachIndexed { index, cal ->
GroupedRow( GroupedRow(
title = cal.displayName, title = cal.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
position = positionOf(index, group.calendars.size), position = positionOf(index, group.calendars.size),
minHeight = 56.dp, minHeight = 56.dp,
leading = { CalendarColorChip(cal.color) }, leading = { CalendarColorChip(cal.color) },
@@ -129,7 +129,7 @@ internal fun HiddenCalendarsStep(
calendars.forEachIndexed { index, calendar -> calendars.forEachIndexed { index, calendar ->
val off = !calendar.isVisibleInSystem val off = !calendar.isVisibleInSystem
GroupedRow( GroupedRow(
title = calendar.displayName, title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
summary = calendar.accountName.takeIf { it.isNotBlank() }, summary = calendar.accountName.takeIf { it.isNotBlank() },
position = when { position = when {
calendars.size == 1 -> Position.Alone calendars.size == 1 -> Position.Alone
@@ -141,7 +141,7 @@ internal fun HiddenCalendarsStep(
trailing = if (calendar.hasVisibilitySwitch) { trailing = if (calendar.hasVisibilitySwitch) {
{ {
CalendarSwitch( CalendarSwitch(
calendarName = calendar.displayName, calendarName = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
enabled = !off, enabled = !off,
onToggle = { on -> onSetVisible(calendar.id, on) }, onToggle = { on -> onSetVisible(calendar.id, on) },
) )
@@ -143,7 +143,7 @@ internal fun EventFormScreen(
durationCalendars.forEachIndexed { index, calendar -> durationCalendars.forEachIndexed { index, calendar ->
val override = state.perCalendarEventDuration[calendar.id] val override = state.perCalendarEventDuration[calendar.id]
GroupedRow( GroupedRow(
title = calendar.displayName, title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
summary = override?.let { durationLabel(it) } summary = override?.let { durationLabel(it) }
?: stringResource( ?: stringResource(
R.string.settings_calendar_duration_inherits, R.string.settings_calendar_duration_inherits,
@@ -181,7 +181,7 @@ internal fun NotificationsScreen(
// own section — link there instead. // own section — link there instead.
if (calendar.id in state.managedCalendarIds) { if (calendar.id in state.managedCalendarIds) {
GroupedRow( GroupedRow(
title = calendar.displayName, title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
summary = stringResource(R.string.settings_calendar_reminders_managed_hint), summary = stringResource(R.string.settings_calendar_reminders_managed_hint),
position = Position.Alone, position = Position.Alone,
leading = { CalendarColorChip(calendar.color) }, leading = { CalendarColorChip(calendar.color) },
@@ -198,7 +198,7 @@ internal fun NotificationsScreen(
} }
val expanded = calendar.id in expandedCalendars val expanded = calendar.id in expandedCalendars
GroupedRow( GroupedRow(
title = calendar.displayName, title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
position = if (expanded) Position.Top else Position.Alone, position = if (expanded) Position.Top else Position.Alone,
leading = { CalendarColorChip(calendar.color) }, leading = { CalendarColorChip(calendar.color) },
trailing = { trailing = {
+3
View File
@@ -290,6 +290,9 @@
<!-- Shared event strings --> <!-- Shared event strings -->
<string name="event_untitled">(No title)</string> <string name="event_untitled">(No title)</string>
<!-- Stands in for a calendar the provider gave no name (#329) -->
<string name="calendar_unnamed">(No name)</string>
<!-- First-launch wizard (#163) --> <!-- First-launch wizard (#163) -->
<string name="onboarding_step_counter">Step %1$d of %2$d</string> <string name="onboarding_step_counter">Step %1$d of %2$d</string>
<string name="onboarding_backup_title">Your events live only here</string> <string name="onboarding_backup_title">Your events live only here</string>
@@ -64,9 +64,9 @@ class CalendarMapperTest {
} }
@Test @Test
fun `null displayName falls back to placeholder`() { fun `null displayName is left blank for the UI to fill in`() {
val src = reader(displayName = null).toCalendarSource() val src = reader(displayName = null).toCalendarSource()
assertThat(src.displayName).isEqualTo(Fallbacks.UNNAMED_CALENDAR) assertThat(src.displayName).isEmpty()
} }
@Test @Test