Localise the unnamed-calendar placeholder (#329)

This commit is contained in:
Jean-Luc Makiola
2026-09-23 22:31:14 +02:00
15 changed files with 38 additions and 31 deletions
@@ -376,7 +376,7 @@ class AndroidCalendarDataSource @Inject constructor(
.build()
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 {
put(CalendarContract.Calendars.ACCOUNT_NAME, LOCAL_ACCOUNT_NAME)
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?) {
val name = displayName.trim().ifEmpty { Fallbacks.UNNAMED_CALENDAR }
val name = displayName.trim()
val values = ContentValues().apply {
put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, name)
put(CalendarContract.Calendars.NAME, name)
@@ -464,7 +464,7 @@ class AndroidCalendarDataSource @Inject constructor(
PackageManager.PERMISSION_GRANTED
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 {
put(CalendarContract.Calendars.ACCOUNT_NAME, LOCAL_ACCOUNT_NAME)
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
return CalendarSource(
id = getLong(CalendarProjection.IDX_ID),
displayName = getString(CalendarProjection.IDX_DISPLAY_NAME)
?: Fallbacks.UNNAMED_CALENDAR,
// Blank when the provider row has no name: the placeholder is a display
// 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(),
accountType = accountType,
color = getInt(CalendarProjection.IDX_COLOR),
@@ -312,7 +312,3 @@ internal object ReminderProjection {
const val IDX_MINUTES = 0
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 ->
val isSelected = calendar.id in selected
GroupedRow(
title = calendar.displayName,
title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
summary = calendar.description,
position = positionOf(index, calendars.size),
leading = { CalendarColorChip(calendar.color) },
@@ -246,7 +246,7 @@ private fun CalendarsList(
local.forEachIndexed { index, calendar ->
val disabled = !calendar.isVisibleInSystem
GroupedRow(
title = calendar.displayName,
title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
summary = calendarRowSummary(calendar),
position = if (index == local.lastIndex) Position.Bottom else Position.Middle,
container = MaterialTheme.colorScheme.surfaceContainerHighest,
@@ -254,7 +254,7 @@ private fun CalendarsList(
leading = { CalendarColorChip(calendar.color, dimIf(disabled)) },
trailing = {
EnableSwitch(
calendarName = calendar.displayName,
calendarName = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
enabled = !disabled,
onToggle = { enabled -> onSetVisible(calendar.id, enabled) },
)
@@ -331,7 +331,7 @@ private fun CalendarsList(
ordered.forEachIndexed { index, calendar ->
val disabled = !calendar.isVisibleInSystem || calendar.isNotSynced
GroupedRow(
title = calendar.displayName,
title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
summary = calendarRowSummary(calendar),
position = if (index == ordered.lastIndex) Position.Bottom else Position.Middle,
container = MaterialTheme.colorScheme.surfaceContainerHighest,
@@ -340,7 +340,7 @@ private fun CalendarsList(
trailing = if (calendar.hasVisibilitySwitch) {
{
EnableSwitch(
calendarName = calendar.displayName,
calendarName = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
enabled = calendar.isVisibleInSystem,
onToggle = { 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).
*/
@Composable
fun accountGroupTitle(group: CalendarAccountGroup): String =
if (!group.ambiguous) {
group.label
fun accountGroupTitle(group: CalendarAccountGroup): String {
val label = group.label.ifBlank { stringResource(R.string.calendar_unnamed) }
return if (!group.ambiguous) {
label
} 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
@@ -116,7 +116,7 @@ private fun CalendarPickerGroup(
calendars.forEachIndexed { index, calendar ->
val isSelected = calendar.id == selectedId
GroupedRow(
title = calendar.displayName,
title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
position = if (index == calendars.lastIndex) Position.Bottom else Position.Middle,
selected = isSelected,
leading = { CalendarColorChip(calendar.color) },
@@ -527,7 +527,8 @@ private fun EventDetailContent(
iconContentDescription = stringResource(R.string.event_detail_calendar),
) {
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,
)
}
@@ -620,7 +620,9 @@ private fun EventEditContent(
Text(
text = stringResource(
R.string.event_edit_managed_hint,
selectedCalendar?.displayName.orEmpty(),
selectedCalendar?.displayName
?.ifBlank { stringResource(R.string.calendar_unnamed) }
.orEmpty(),
),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -719,6 +721,7 @@ private fun EventEditContent(
) {
Text(
text = selectedCalendar?.displayName
?.ifBlank { stringResource(R.string.calendar_unnamed) }
?: stringResource(R.string.event_edit_error_no_calendar),
style = MaterialTheme.typography.titleMedium,
color = if (selectedCalendar == null) {
@@ -62,15 +62,16 @@ private fun FilterList(
) {
Column(modifier = modifier.fillMaxWidth()) {
groups.forEach { group ->
val account = group.account.ifBlank { stringResource(R.string.calendar_unnamed) }
Text(
text = if (group.ambiguous) {
stringResource(
R.string.calendars_account_from_source,
group.account,
account,
sourceAppName(group.accountType),
)
} else {
group.account
account
},
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -83,7 +84,7 @@ private fun FilterList(
)
group.calendars.forEachIndexed { index, cal ->
GroupedRow(
title = cal.displayName,
title = cal.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
position = positionOf(index, group.calendars.size),
minHeight = 56.dp,
leading = { CalendarColorChip(cal.color) },
@@ -129,7 +129,7 @@ internal fun HiddenCalendarsStep(
calendars.forEachIndexed { index, calendar ->
val off = !calendar.isVisibleInSystem
GroupedRow(
title = calendar.displayName,
title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
summary = calendar.accountName.takeIf { it.isNotBlank() },
position = when {
calendars.size == 1 -> Position.Alone
@@ -141,7 +141,7 @@ internal fun HiddenCalendarsStep(
trailing = if (calendar.hasVisibilitySwitch) {
{
CalendarSwitch(
calendarName = calendar.displayName,
calendarName = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
enabled = !off,
onToggle = { on -> onSetVisible(calendar.id, on) },
)
@@ -143,7 +143,7 @@ internal fun EventFormScreen(
durationCalendars.forEachIndexed { index, calendar ->
val override = state.perCalendarEventDuration[calendar.id]
GroupedRow(
title = calendar.displayName,
title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
summary = override?.let { durationLabel(it) }
?: stringResource(
R.string.settings_calendar_duration_inherits,
@@ -181,7 +181,7 @@ internal fun NotificationsScreen(
// own section — link there instead.
if (calendar.id in state.managedCalendarIds) {
GroupedRow(
title = calendar.displayName,
title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
summary = stringResource(R.string.settings_calendar_reminders_managed_hint),
position = Position.Alone,
leading = { CalendarColorChip(calendar.color) },
@@ -198,7 +198,7 @@ internal fun NotificationsScreen(
}
val expanded = calendar.id in expandedCalendars
GroupedRow(
title = calendar.displayName,
title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
position = if (expanded) Position.Top else Position.Alone,
leading = { CalendarColorChip(calendar.color) },
trailing = {
+3
View File
@@ -290,6 +290,9 @@
<!-- Shared event strings -->
<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) -->
<string name="onboarding_step_counter">Step %1$d of %2$d</string>
<string name="onboarding_backup_title">Your events live only here</string>
@@ -64,9 +64,9 @@ class CalendarMapperTest {
}
@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()
assertThat(src.displayName).isEqualTo(Fallbacks.UNNAMED_CALENDAR)
assertThat(src.displayName).isEmpty()
}
@Test