Compare commits

..

3 Commits

Author SHA1 Message Date
c0b7ad29ef docs: trim the #234 comments to the load-bearing facts
The detach path's KDoc and inline comments restated the bug narrative that
already lives in the commit message and the issue. Keep what a reader of
the code needs — why the branch exists, what the detached row loses, and
why the insert precedes the EXDATE update — and drop the rest.
2026-08-27 20:21:34 +02:00
bc49730ea5 fix: guard the detach path against a second copy, and say what it costs (#234)
Review pass on the detach path. Three changes, none to the write shape itself.

A second detach of the same occurrence is now refused. Reached from a stale
screen still pointing at the parent, it used to succeed twice over: the EXDATE
merge folds the repeated stamp away and the parent update still reports one row
changed, so the save looked fine and left a *second* standalone copy of one
occurrence, this time with neither copy in the series to make it obvious. The
occurrence is already gone from the series at that point, so the write reports
it as gone — and EventEditViewModel now maps NoSuchEventException from the write
to the same "this event no longer exists" answer its pre-check already gives,
instead of a bare "couldn't save" for something that isn't there.

The rollback comment claimed more than the code delivers. ContentResolver.update
returns rows touched, not occurrences excluded, so the zero check catches the
series row vanishing mid-write and nothing else — an EXDATE the provider's
expansion fails to match reports success and leaves the duplicate standing.
Renamed the variable to match, and the catch now mirrors moveEvent's Throwable
idiom rather than inventing a narrower one for the same insert-then-roll-back
shape.

The rest is honesty about what a detached row can't do, since none of it is
recoverable later and the KDoc previously mentioned only the calendar-move case:
a whole-series delete leaves it standing where an exception row would have gone
with the parent; a series-wide *time* edit moves the generated instances but not
the absolute-instant EXDATE hole, so the occurrence returns alongside its copy;
and building the row from the form rather than cloning the parent drops
ORGANIZER, STATUS and the organizer/resource attendee rows, exactly as moveEvent
does. The EXDATE staleness is not new — a #47 delete resurrects the same way —
but a duplicate is a louder symptom than a resurrection, and it wants fixing at
the series-update end, not here.

Also notes why the branch predicate is stricter than deleteOccurrence's: EXDATE
only means something on a row that recurs, so a row without an RRULE keeps the
old path rather than having a recurrence set written onto a one-off event.

Tests: the tautological "detached == copy(rrule = null)" assertion is replaced by
one that pins every edited field through to the inserted columns, and
exdateContains gets its own cases (timed, all-day, absent, a neighbouring
occurrence, and a multi-entry list with whitespace).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 20:10:22 +02:00
a1d1894f84 fix: detach the occurrence when editing one instance of an unsynced series (#234)
"Edit only this event" wrote a modified-occurrence exception unconditionally.
That is the right shape for a synced series and the wrong one for everything
else: an exception attaches to its parent through ORIGINAL_SYNC_ID, so on a row
with no _sync_id the link never forms — the insert fails or lands an orphan, the
generic catch in EventEditViewModel turns it into a snackbar, and the scope
dialog just closes again. To the reporter that read as "nothing happens, ever",
with a stray copy of the event left behind the one time the insert did land.

This is the same constraint deleteOccurrence has documented since #47, and the
same calendars: a local calendar, Calendula's own contact special-date
calendars, and — the reporter's case — a Google calendar whose rows the sync
adapter has not stamped yet, which is exactly why it "shows as on-device".

updateOccurrence now branches on _sync_id the way deleteOccurrence does. A
synced series keeps the exception path untouched; its write shape is load-
bearing and verified. A series without one gets the occurrence excluded from the
parent via EXDATE and the edited values inserted as a standalone event on the
same calendar — a detached instance, minus the RECURRENCE-ID the provider has no
way to store here. The cost is honest and worth naming: the edited occurrence
stops travelling with its series. The alternative is an edit that silently does
nothing.

Two things shape the write. The parent update reuses buildOccurrenceExdateValues
unchanged, so it keeps carrying the whole time/recurrence set — an EXDATE-only
update is not a recurrence change to the provider and leaves the expanded
instances standing (#47's first quirk). And the form's RRULE is stripped before
the insert: the exception path gets an inherited rule cleared for free by
DTSTART + DURATION, but nothing clears one here, so leaving it would insert a
second *series* overlapping the first.

Ordering is chosen for the failure cases, not the happy path. The insert runs
first, so a failure there leaves the series completely untouched — the discipline
updateEventFromOccurrence already follows. If the EXDATE update then fails, the
new row is a visible duplicate of an occurrence still in the series, so it is
rolled back (best effort) before the failure surfaces. The reverse order could
strand an occurrence excluded from its series with nothing standing in for it,
turning an edit into a silent delete.

Also makes a failed save legible, since this bug was invisible precisely because
it wasn't: the failure snackbar gets the long duration instead of a flash, and
the catch logs the scope and event id — never the form's content — so a failure
leaves something to report.

Adds JVM tests for the detached shape: the rule is dropped and the row becomes a
one-off with DTEND, all-day stays on UTC midnights, and the detached row's
DTSTART agrees with the EXDATE stamp that removes it from the parent, timed and
all-day. The provider behaviour itself still needs a device.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 19:57:35 +02:00
10 changed files with 419 additions and 793 deletions

View File

@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Fixed
- **"Only this event" now actually saves your edit.** On some calendars —
including Google ones that still show as on-device, and any local calendar —
editing a single occurrence of a repeating event did nothing at all: the scope
dialog closed, the edit screen stayed put, and saving again just repeated it.
Android can only attach a single-occurrence change to its series once the
calendar has been synced at least once, so on those calendars the change had
nowhere to go. Calendula now removes that one occurrence from the series and
saves the edit as its own event instead, which is what you see either way. A
save that does fail also says so for longer, rather than flashing past
([#234]).
## [2.19.3] — 2026-08-22 ## [2.19.3] — 2026-08-22
### Added ### Added
@@ -1471,3 +1483,4 @@ automatically, with zero telemetry and no internet permission.
[#192]: https://codeberg.org/jlmakiola/calendula/issues/192 [#192]: https://codeberg.org/jlmakiola/calendula/issues/192
[#196]: https://codeberg.org/jlmakiola/calendula/issues/196 [#196]: https://codeberg.org/jlmakiola/calendula/issues/196
[#214]: https://codeberg.org/jlmakiola/calendula/issues/214 [#214]: https://codeberg.org/jlmakiola/calendula/issues/214
[#234]: https://codeberg.org/jlmakiola/calendula/issues/234

View File

@@ -232,10 +232,15 @@ interface CalendarDataSource {
): Long ): Long
/** /**
* Change a single occurrence of a recurring event by inserting a * Change a single occurrence of a recurring event at [beginMillis] (the
* modified-occurrence exception at [beginMillis] (the occurrence's * occurrence's `Instances.BEGIN`) to [form]'s values; returns the
* `Instances.BEGIN`) carrying [form]'s values; returns the exception * `Events._ID` of the row now holding them.
* row's `Events._ID`. [allDayReminderTimeMinutes]: see [insertEvent]. *
* A series with a `_sync_id` gets a modified-occurrence exception. One
* without gets the occurrence excluded from the parent via EXDATE plus a
* standalone event carrying the edits — an exception cannot link to its
* parent there (Codeberg #234, the same constraint as [deleteOccurrence]).
* [allDayReminderTimeMinutes]: see [insertEvent].
*/ */
fun updateOccurrence( fun updateOccurrence(
eventId: Long, eventId: Long,
@@ -1194,6 +1199,13 @@ class AndroidCalendarDataSource @Inject constructor(
form: EventForm, form: EventForm,
allDayReminderTimeMinutes: Int, allDayReminderTimeMinutes: Int,
): Long { ): Long {
val row = querySeriesRow(eventId)
// Stricter than deleteOccurrence's bare _sync_id check: EXDATE only means
// something on a row that recurs, so a non-recurring one keeps the
// exception path rather than getting a recurrence set written onto it.
if (row.syncId == null && !row.rrule.isNullOrBlank()) {
return detachOccurrence(eventId, beginMillis, row, form, allDayReminderTimeMinutes)
}
// The provider clones the series row and applies these values on top. // The provider clones the series row and applies these values on top.
val values = buildOccurrenceExceptionValues( val values = buildOccurrenceExceptionValues(
form = form, form = form,
@@ -1212,6 +1224,89 @@ class AndroidCalendarDataSource @Inject constructor(
return exceptionId return exceptionId
} }
/**
* "Edit only this event" on a series with **no `_sync_id`**: drop the
* occurrence from the parent with EXDATE and insert the edited values as a
* standalone event on the same calendar.
*
* A modified exception attaches to its parent only through `ORIGINAL_SYNC_ID`,
* exactly like the cancelled one [deleteOccurrence] documents; with no
* `_sync_id` the link never forms and the edit is lost (Codeberg #234).
* EXDATE plus a standalone row needs no link — what a detached instance
* degrades to without a `RECURRENCE-ID` to carry it.
*
* The detached row keeps no stored link back to its series, so: it no longer
* travels with it ([moveEvent] copies the master and its `ORIGINAL_ID`
* children, and this is neither); its EXDATE hole is an absolute instant, so
* re-timing the whole series brings the occurrence back beside the copy (a
* #47 delete resurrects the same way); and it is built from the form, not
* cloned, so `ORGANIZER`, `STATUS` and the attendee rows [reconcileAttendees]
* preserves are dropped — the same limitation as [moveEvent].
*
* Insert first, so a failure leaves the series untouched
* ([updateEventFromOccurrence]'s discipline); roll the new row back if the
* EXDATE update then fails, since it would be a visible duplicate. The
* reverse order risks the worse outcome — an excluded occurrence with no
* replacement, i.e. an edit that quietly deletes.
*/
private fun detachOccurrence(
eventId: Long,
beginMillis: Long,
row: SeriesRow,
form: EventForm,
allDayReminderTimeMinutes: Int,
): Long {
// Already detached (or deleted) from a stale screen still pointing at the
// parent: the EXDATE merge would fold the repeat away and still report a
// changed row, quietly leaving a *second* standalone copy.
if (exdateContains(row.exdate, beginMillis, isAllDay = row.allDay != 0)) {
throw NoSuchEventException(eventId)
}
// Reminders, guests and colour come along like any new event, and so does
// a fresh UID — the detached row is a separate event now, and sharing the
// parent's would collide with it in .ics restore dedup.
val detachedId = insertEvent(form.toDetachedOccurrence(), allDayReminderTimeMinutes)
val values = buildOccurrenceExdateValues(
existingExdate = row.exdate,
occurrenceMillis = beginMillis,
dtStartMillis = row.dtStartMillis,
rrule = row.rrule,
duration = row.duration,
timezone = row.timezone,
allDay = row.allDay,
)
// Rows touched, not occurrences excluded — 1 whenever the series row still
// exists. It catches the row disappearing under us, not an EXDATE the
// provider's expansion fails to match.
val updatedRows = try {
resolver.update(
ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId),
values.toContentValues(), null, null,
)
} catch (t: Throwable) {
rollBackDetached(detachedId)
throw t
}
if (updatedRows == 0) {
rollBackDetached(detachedId)
throw WriteFailedException(
"exdate occurrence for edit, event id=$eventId begin=$beginMillis",
)
}
return detachedId
}
/**
* Undo the standalone row [detachOccurrence] inserted before its EXDATE
* update failed. Best effort: the caller is already throwing, and the worst
* case is the duplicate we were avoiding — never a lost occurrence.
*/
private fun rollBackDetached(detachedId: Long) {
runCatching { deleteEvent(detachedId) }.onFailure {
Log.w(TAG, "Failed to roll back detached occurrence $detachedId", it)
}
}
override fun updateEventFromOccurrence( override fun updateEventFromOccurrence(
eventId: Long, eventId: Long,
beginMillis: Long, beginMillis: Long,

View File

@@ -243,6 +243,21 @@ internal fun buildOccurrenceExceptionValues(
putAll(eventColorColumns(form.colorKey, form.color)) putAll(eventColorColumns(form.colorKey, form.color))
} }
/**
* The form as a **detached occurrence**: the same edited values, with the
* series rule dropped so [buildEventInsertValues] writes a standalone one-off
* row (DTSTART + DTEND, no RRULE/DURATION) at the occurrence's own times.
*
* The "edit only this event" shape for a series with **no `_sync_id`**, where an
* exception row can't attach to its parent at all (Codeberg #234).
*
* The exception path gets the rule dropped for free — the provider clears the
* RRULE it cloned when an exception carries DTSTART + DURATION
* ([buildOccurrenceExceptionValues]). Here nothing is cloned, so it is stripped
* by hand; leaving it on would insert a second *series* overlapping the first.
*/
internal fun EventForm.toDetachedOccurrence(): EventForm = copy(rrule = null)
/** /**
* Raw provider snapshot of a master/one-off Events row, enough to re-insert it * Raw provider snapshot of a master/one-off Events row, enough to re-insert it
* verbatim on another calendar (a calendar move is copy+delete — `CALENDAR_ID` * verbatim on another calendar (a calendar move is copy+delete — `CALENDAR_ID`
@@ -435,6 +450,23 @@ internal fun buildOccurrenceExdateValues(
) )
} }
/**
* Whether [existingExdate] already excludes the occurrence at [occurrenceMillis]
* — i.e. it has already been dropped from the series, deleted or detached.
*
* Guards the detach path against running twice from a stale screen: the EXDATE
* merge folds the repeat away silently and the update still reports one row
* changed, so a second save would leave a second standalone copy.
*/
internal fun exdateContains(
existingExdate: String?,
occurrenceMillis: Long,
isAllDay: Boolean,
): Boolean {
val stamp = formatExdateStamp(occurrenceMillis, isAllDay)
return existingExdate?.split(',')?.any { it.trim() == stamp } == true
}
/** /**
* One EXDATE entry for the occurrence starting at [occurrenceMillis]. Both forms * One EXDATE entry for the occurrence starting at [occurrenceMillis]. Both forms
* are UTC: the provider stores an all-day DTSTART at UTC midnight, so its date * are UTC: the provider stores an all-day DTSTART at UTC midnight, so its date

View File

@@ -64,6 +64,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@@ -264,13 +265,16 @@ fun EventEditScreen(
viewModel.reset() viewModel.reset()
onSaved() onSaved()
} }
// A failed save leaves the form looking unchanged, so the snackbar is
// the only sign anything happened — long rather than the default
// flash (Codeberg #234: it read as "nothing happens at all").
SaveUiState.Failed -> { SaveUiState.Failed -> {
viewModel.consumeSaveResult() viewModel.consumeSaveResult()
snackbarHostState.showSnackbar(saveFailedMessage) snackbarHostState.showSnackbar(saveFailedMessage, duration = SnackbarDuration.Long)
} }
SaveUiState.NeedsPermission -> { SaveUiState.NeedsPermission -> {
viewModel.consumeSaveResult() viewModel.consumeSaveResult()
snackbarHostState.showSnackbar(writeDeniedMessage) snackbarHostState.showSnackbar(writeDeniedMessage, duration = SnackbarDuration.Long)
} }
// AwaitingScope/AwaitingConflict/Gone render as dialogs below. // AwaitingScope/AwaitingConflict/Gone render as dialogs below.
else -> Unit else -> Unit

View File

@@ -1,5 +1,6 @@
package de.jeanlucmakiola.calendula.ui.edit package de.jeanlucmakiola.calendula.ui.edit
import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@@ -56,6 +57,8 @@ import kotlin.time.Duration.Companion.minutes
import kotlin.time.Instant import kotlin.time.Instant
import javax.inject.Inject import javax.inject.Inject
private const val TAG = "EventEdit"
/** /**
* Where a prefilled [EventEditViewModel.openImported] form came from. The sources * Where a prefilled [EventEditViewModel.openImported] form came from. The sources
* want different reminder handling (#49), and differ in whether they own the * want different reminder handling (#49), and differ in whether they own the
@@ -751,7 +754,15 @@ class EventEditViewModel @Inject constructor(
throw e throw e
} catch (e: SecurityException) { } catch (e: SecurityException) {
SaveUiState.NeedsPermission SaveUiState.NeedsPermission
} catch (e: NoSuchEventException) {
// The event or occurrence is already gone: the same answer the
// pre-check gives, and better than a bare "couldn't save".
SaveUiState.Gone
} catch (e: Exception) { } catch (e: Exception) {
// The user only gets a generic snackbar, so without this a failed
// write leaves nothing to report (Codeberg #234). Scope and event
// id only — never the form's content.
Log.w(TAG, "Save failed (scope=$scope, eventId=${target?.eventId})", e)
SaveUiState.Failed SaveUiState.Failed
} }
} }

View File

@@ -43,25 +43,25 @@
<string name="day_today_action">Dnes</string> <string name="day_today_action">Dnes</string>
<string name="event_detail_back">Zpět</string> <string name="event_detail_back">Zpět</string>
<string name="event_detail_edit">Upravit</string> <string name="event_detail_edit">Upravit</string>
<string name="event_detail_delete">Odstranit</string> <string name="event_detail_delete">Vymazat</string>
<string name="event_detail_share">Sdílet</string> <string name="event_detail_share">Sdílet</string>
<string name="event_detail_duplicate">Kopírovat</string> <string name="event_detail_duplicate">Kopírovat</string>
<string name="event_share_chooser_title">Sdílet událost</string> <string name="event_share_chooser_title">Sdílet událost</string>
<string name="event_share_failed">Tuto událost nelze sdílet.</string> <string name="event_share_failed">Tuto událost nelze sdílet.</string>
<string name="event_delete_title">Odstranit událost?</string> <string name="event_delete_title">Odstranit událost?</string>
<string name="event_delete_body">Událost je odstraněna z kalendáře i ze všech synchronizovaných zařízení.</string> <string name="event_delete_body">Událost je odstraněna z kalendáře i ze všech synchronizovaných zařízení.</string>
<string name="event_delete_recurring_title">Odstranit opakující se událost</string> <string name="event_delete_recurring_title">Smazat opakující se událost</string>
<string name="event_delete_option_occurrence">Pouze tuto událost</string> <string name="event_delete_option_occurrence">Pouze tuto událost</string>
<string name="event_delete_option_following">Tuto a všechny následující události</string> <string name="event_delete_option_following">Tuto a všechny následující události</string>
<string name="event_delete_option_series">Všechny události v sérii</string> <string name="event_delete_option_series">Všechny události v sérii</string>
<string name="event_edit_recurring_title">Upravit opakující se událost</string> <string name="event_edit_recurring_title">Upravit opakující se událost</string>
<string name="event_delete_failed">Tuto událost nelze odstranit</string> <string name="event_delete_failed">Tuto událost nelze odstranit</string>
<string name="event_delete_write_denied">Calendula potřebuje oprávnění k zápisu, aby mohla odstraňovat události</string> <string name="event_delete_write_denied">Calendula potřebuje oprávnění k zápisu, aby mohla mazat události</string>
<string name="event_move_action">Přesunout…</string> <string name="event_move_action">Přesunout…</string>
<string name="event_move_recurring_title">Přesunout opakující se událost</string> <string name="event_move_recurring_title">Přesunout opakující se událost</string>
<string name="event_move_occurrence_only">Přesunutí celé série by rozhodilo pořadí dnů, na které série připadá, přesunout lze tedy pouze tuto událost.</string> <string name="event_move_occurrence_only">Přesunutí celé série by rozhodilo pořadí dnů, na které série připadá, přesunout lze tedy pouze tuto událost.</string>
<string name="event_move_done">Přesunuto do %1$s</string> <string name="event_move_done">Přesunuto do %1$s</string>
<string name="event_move_undo">Vrátit</string> <string name="event_move_undo">Zpět</string>
<string name="event_move_undone">Vráceno zpět</string> <string name="event_move_undone">Vráceno zpět</string>
<string name="event_move_failed">Událost nelze přesunout</string> <string name="event_move_failed">Událost nelze přesunout</string>
<string name="event_move_write_denied">Calendula k přesouvání události vyžaduje oprávnění k zápisu</string> <string name="event_move_write_denied">Calendula k přesouvání události vyžaduje oprávnění k zápisu</string>
@@ -111,7 +111,7 @@
<string name="event_edit_color">Barva</string> <string name="event_edit_color">Barva</string>
<string name="event_edit_color_default">Barva kalendáře</string> <string name="event_edit_color_default">Barva kalendáře</string>
<string name="event_edit_color_custom">Vlastní barva</string> <string name="event_edit_color_custom">Vlastní barva</string>
<string name="event_edit_color_reset">Znovu nastavit</string> <string name="event_edit_color_reset">Obnovit</string>
<string name="event_edit_color_unsupported">Pro tento kalendář není k dispozici</string> <string name="event_edit_color_unsupported">Pro tento kalendář není k dispozici</string>
<string name="event_edit_color_unsupported_hint">Tento kalendář nemá žádnou sadu barev. Vlastní barvy kalendářů můžete povolit v nastavení.</string> <string name="event_edit_color_unsupported_hint">Tento kalendář nemá žádnou sadu barev. Vlastní barvy kalendářů můžete povolit v nastavení.</string>
<string name="event_edit_color_sync_warning">Tato barva může být během příští synchronizace kalendáře ztracena nebo přepsána.</string> <string name="event_edit_color_sync_warning">Tato barva může být během příští synchronizace kalendáře ztracena nebo přepsána.</string>
@@ -121,8 +121,8 @@
<string name="event_edit_conflict_overwrite_hint">Pouze u upravených polí dojde k přepsání vnějšího zdroje</string> <string name="event_edit_conflict_overwrite_hint">Pouze u upravených polí dojde k přepsání vnějšího zdroje</string>
<string name="event_edit_conflict_discard">Zrušit změny</string> <string name="event_edit_conflict_discard">Zrušit změny</string>
<string name="event_edit_conflict_discard_hint">Událost zůstává beze změny</string> <string name="event_edit_conflict_discard_hint">Událost zůstává beze změny</string>
<string name="event_edit_gone_title">Událost byla odstraněna</string> <string name="event_edit_gone_title">Událost byla smazána</string>
<string name="event_edit_gone_body">Během úpravy došlo k odstranění události, například pomocí jiného zařízení. Vaše změny již nelze uložit.</string> <string name="event_edit_gone_body">Během úpravy došlo ke smazání události, například pomocí jiného zařízení. Vaše změny již nelze uložit.</string>
<string name="import_reminder_prompt_title">Chcete použít výchozí upomínku?</string> <string name="import_reminder_prompt_title">Chcete použít výchozí upomínku?</string>
<string name="import_reminder_prompt_body_none">Tato událost byla importována bez nastavené upomínky.</string> <string name="import_reminder_prompt_body_none">Tato událost byla importována bez nastavené upomínky.</string>
<plurals name="import_reminder_prompt_body_existing"> <plurals name="import_reminder_prompt_body_existing">
@@ -250,86 +250,4 @@
<string name="onboarding_backup_benefit_folder_title">Složka, kterou si vyberete</string> <string name="onboarding_backup_benefit_folder_title">Složka, kterou si vyberete</string>
<string name="onboarding_backup_benefit_folder_body">Zálohy jsou obyčejné soubory v .ics formátu — uložte je někam, kde se synchronizují, nebo na SD kartu.</string> <string name="onboarding_backup_benefit_folder_body">Zálohy jsou obyčejné soubory v .ics formátu — uložte je někam, kde se synchronizují, nebo na SD kartu.</string>
<string name="onboarding_backup_benefit_daily_title">Jednou denně, automaticky</string> <string name="onboarding_backup_benefit_daily_title">Jednou denně, automaticky</string>
<string name="onboarding_backup_benefit_daily_body">Calendula exportuje vaše místní kalendáře na pozadí. Frekvenci lze změnit v nastavení.</string>
<string name="onboarding_backup_enable_button">Vyberte složku a proveďte zálohu</string>
<string name="onboarding_backup_skip_button">Teď ne</string>
<string name="onboarding_view_title">Co by se mělo otevřít nejdříve?</string>
<string name="onboarding_month_style_title">Jak by mělo vypadat měsíční rozvržení?</string>
<string name="onboarding_view_continue_button">Pokračovat</string>
<string name="onboarding_back">Zpět</string>
<string name="onboarding_visibility_button">Jasně</string>
<string name="onboarding_done_title">Vše je připraveno</string>
<string name="onboarding_done_body">Vaše kalendáře jsou připraveny. Veškeré volby lze později změnit v nastavení.</string>
<string name="onboarding_done_button">Otevřít můj kalendář</string>
<string name="reminder_channel_name">Upomínky událostí</string>
<string name="reminder_channel_description">Oznámení v časech upomínek vašich událostí</string>
<string name="reminder_onboarding_title">Ať vám neunikne žádná událost</string>
<string name="reminder_onboarding_body">Android sám o sobě nezobrazuje upomínky událostí — to musí zajistit aplikace kalendáře. Nechte tuto práci na Calendule.</string>
<string name="reminder_benefit_delivery_title">Upomínky, doručené</string>
<string name="reminder_benefit_delivery_body">Upomínky k vašim událostem vám přijdou v danou dobu jako oznámení.</string>
<string name="reminder_benefit_duplicates_title">Používáte ještě nějakou jinou aplikaci s kalendářem?</string>
<string name="reminder_benefit_duplicates_body">Pokud i jiná aplikace odesílá upomínky, uvidíte je dvakrát — v jedné z aplikací je musíte vypnout.</string>
<string name="reminder_benefit_reversible_title">Lze kdykoliv změnit</string>
<string name="reminder_benefit_reversible_body">Tento přepínač naleznete v Nastavení, v části Oznámení.</string>
<string name="reminder_onboarding_enable_button">Zapnout upomínky</string>
<string name="reminder_onboarding_skip_button">Později</string>
<string name="reminder_action_snooze">Odložit</string>
<string name="reminder_action_dismiss">Zrušit</string>
<string name="reminder_day_tomorrow">Zítra</string>
<string name="reminder_day_yesterday">Včera</string>
<string name="view_month">Měsíc</string>
<string name="view_week">Týden</string>
<string name="view_day">Den</string>
<string name="view_agenda">Program</string>
<string name="view_section">Zobrazení</string>
<string name="drawer_jump_to_date">Přejít na datum</string>
<string name="agenda_today_action">Dnes</string>
<string name="agenda_header_today">Dnes</string>
<string name="agenda_header_tomorrow">Zítra</string>
<string name="agenda_empty_title">Teď už jsi v obraze</string>
<string name="agenda_no_more_today">Dnes bez dalších událostí</string>
<string name="agenda_span_starts">Začátek %1$s</string>
<string name="agenda_span_ends">Konec %1$s</string>
<string name="today_jump_action">Přejít na dnešek</string>
<string name="search_action">Hledat</string>
<string name="search_hint">Vyhledat události</string>
<string name="search_back">Zpět</string>
<string name="search_clear">Vymazat</string>
<string name="search_idle_hint">Prohledávejte své události podle názvu, místa konání nebo poznámek.</string>
<string name="search_empty">Žádná událost neodpovídá “%1$s”.</string>
<plurals name="search_selected_count">
<item quantity="one">%d označený</item>
<item quantity="few">%d označené</item>
<item quantity="many">%d označených</item>
<item quantity="other">%d označených</item>
</plurals>
<string name="search_in_descriptions">Nalezeno v popisech</string>
<string name="search_selection_close">Zrušit výběr</string>
<string name="search_select_all">Vybrat vše</string>
<string name="search_delete_selected">Odstranit vybrané</string>
<plurals name="search_delete_title">
<item quantity="one">Odstranit %d událost?</item>
<item quantity="few">Odstranit %d události?</item>
<item quantity="many">Odstranit %d událostí?</item>
<item quantity="other">Odstranit %d událostí?</item>
</plurals>
<plurals name="search_delete_done">
<item quantity="one">%d událost odstraněna</item>
<item quantity="few">%d události odstraněny</item>
<item quantity="many">%d událostí odstraněno</item>
<item quantity="other">%d událostí odstraněno</item>
</plurals>
<string name="search_delete_partial">%1$d odstraněno, u %2$d se nepodařilo</string>
<string name="search_delete_denied_partial">%1$d odstraněno, poté byl odepřen přístup pro zápis</string>
<string name="settings_special_dates_disable_all_message">Tímto se smažou kalendáře spojené s určitým kontaktem a všechny události v nich obsažené. Veškerá upomínky a poznámky, které jste do nich přidali, budou ztraceny.</string>
<string name="widget_agenda_title">Nadcházející</string>
<string name="widget_agenda_label">Události Calendula</string>
<string name="widget_month_label">Měsíc Calendula</string>
<string name="widget_refresh">Obnovit</string>
<string name="widget_new_event">Nová událost</string>
<string name="widget_needs_permission">Otevřete Calendulu a povolte přístup ke kalendáři</string>
<string name="widget_prev_month">Předchozí měsíc</string>
<string name="widget_next_month">Následující měsíc</string>
<string name="widget_today">Dnes</string>
<string name="filter_title">Kalendáře</string>
</resources> </resources>

View File

@@ -16,14 +16,14 @@
<string name="state_failure_no_calendars_action">Otwórz systemowe ustawienia kalendarza</string> <string name="state_failure_no_calendars_action">Otwórz systemowe ustawienia kalendarza</string>
<string name="state_failure_provider">Nie udało się kalendarza.</string> <string name="state_failure_provider">Nie udało się kalendarza.</string>
<string name="permission_rationale_title">Zobacz wszystkie swoje wydarzenia w pięknej oprawie</string> <string name="permission_rationale_title">Zobacz wszystkie swoje wydarzenia w pięknej oprawie</string>
<string name="permission_rationale_body">Calendula potrzebuje dostępu do Twojego kalendarza, aby wyświetlać wydarzenia i nimi zarządzać.</string> <string name="permission_rationale_body">Calendula potrzebuje dostępu do Twojego kalendarza, aby wyświetlać wydarzenia i nimi zarządzać. To jedyne, o co prosi na wstępie — i żadne dane nigdy nie opuszczają Twojego urządzenia.</string>
<string name="permission_request_button">Przyznaj dostęp do kalendarza</string> <string name="permission_request_button">Przyznaj dostęp do kalendarza</string>
<string name="permission_denied_title">Brak dostępu do kalendarza</string> <string name="permission_denied_title">Brak dostępu do kalendarza</string>
<string name="permission_denied_body">Calendula nie może wyświetlać wydarzeń bez dostępu do kalendarza. Możesz go ponownie przyznać w ustawieniach systemowych.</string> <string name="permission_denied_body">Calendula nie może wyświetlać wydarzeń bez dostępu do kalendarza. Możesz go ponownie przyznać w ustawieniach systemowych.</string>
<string name="permission_open_settings_button">Otwórz ustawienia systemowe</string> <string name="permission_open_settings_button">Otwórz ustawienia systemowe</string>
<string name="permission_retry_button">Spróbuj ponownie</string> <string name="permission_retry_button">Spróbuj ponownie</string>
<string name="permission_benefit_private_title">Prywatność z założenia</string> <string name="permission_benefit_private_title">Pozostaje na Twoim urządzeniu</string>
<string name="permission_benefit_private_body">Brak uprawnień do internetu, brak telemetrii — nic nigdy nie opuszcza Twojego telefonu.</string> <string name="permission_benefit_private_body">Twoje kalendarze są odczytywane lokalnie i nigdy nie opuszcza telefonu.</string>
<string name="permission_benefit_sync_title">Wszystkie Twoje kalendarze w jednym miejscu</string> <string name="permission_benefit_sync_title">Wszystkie Twoje kalendarze w jednym miejscu</string>
<string name="permission_benefit_sync_body">Google, CalDAV, lokalne — wszystko, co jest zsynchronizowane z urządzeniem, po prostu się pojawia.</string> <string name="permission_benefit_sync_body">Google, CalDAV, lokalne — wszystko, co jest zsynchronizowane z urządzeniem, po prostu się pojawia.</string>
<string name="permission_benefit_privacy_title">Nigdy żadnego śledzenia</string> <string name="permission_benefit_privacy_title">Nigdy żadnego śledzenia</string>
@@ -333,7 +333,7 @@
<string name="settings_default_reminder">Domyślne przypomnienie</string> <string name="settings_default_reminder">Domyślne przypomnienie</string>
<string name="settings_default_reminder_allday">Wydarzenia całodniowe</string> <string name="settings_default_reminder_allday">Wydarzenia całodniowe</string>
<string name="settings_allday_reminder_time">Godzina przypomnienia o wydarzeniach całodniowych</string> <string name="settings_allday_reminder_time">Godzina przypomnienia o wydarzeniach całodniowych</string>
<string name="settings_allday_reminder_time_hint">Przypomnienia o wydarzeniach całodniowych pojawiają się o %1$s</string> <string name="settings_allday_reminder_time_hint">Przypomnienia o wydarzeniach całodniowych zostaną pojawiają się o %1$s</string>
<string name="reminder_none">Brak</string> <string name="reminder_none">Brak</string>
<string name="reminder_use_default">Użyj domyślnego przypomnienia</string> <string name="reminder_use_default">Użyj domyślnego przypomnienia</string>
<string name="reminder_custom_amount">jednostek</string> <string name="reminder_custom_amount">jednostek</string>
@@ -353,10 +353,10 @@
<string name="settings_language_auto">Domyślny systemu</string> <string name="settings_language_auto">Domyślny systemu</string>
<string name="settings_translate">Pomóż w tłumaczeniu</string> <string name="settings_translate">Pomóż w tłumaczeniu</string>
<string name="settings_translate_hint">Dodaj lub ulepsz tłumaczenie w Weblate</string> <string name="settings_translate_hint">Dodaj lub ulepsz tłumaczenie w Weblate</string>
<string name="settings_appearance_subtitle">Motyw, kolory, czcionki</string> <string name="settings_appearance_subtitle">Motyw, domyślny widok, pierwszy dzień tygodnia</string>
<string name="settings_views_subtitle">Domyślny widok, układ, kolejność</string> <string name="settings_views_subtitle">Układ widoku miesiąca, przycisk szybkiego przełączania, kolejność menu</string>
<string name="settings_event_form_subtitle">Domyślne pola i zachowanie</string> <string name="settings_event_form_subtitle">Domyślne pola dla nowych wydarzeń</string>
<string name="settings_notifications_subtitle">Przypomnienia i dostarczanie</string> <string name="settings_notifications_subtitle">Przypomnienia o wydarzeniach</string>
<string name="settings_special_dates_subtitle">Urodziny i rocznice kontaktów</string> <string name="settings_special_dates_subtitle">Urodziny i rocznice kontaktów</string>
<string name="settings_section_special_dates">Szczególne daty kontaktów</string> <string name="settings_section_special_dates">Szczególne daty kontaktów</string>
<string name="settings_special_dates_enable">Pokaż daty kontaktów</string> <string name="settings_special_dates_enable">Pokaż daty kontaktów</string>
@@ -509,8 +509,8 @@
<string name="reminder_day_tomorrow">Jutro</string> <string name="reminder_day_tomorrow">Jutro</string>
<string name="reminder_day_yesterday">Wczoraj</string> <string name="reminder_day_yesterday">Wczoraj</string>
<string name="agenda_no_more_today">Brak zaplanowanych wydarzeń</string> <string name="agenda_no_more_today">Brak zaplanowanych wydarzeń</string>
<string name="settings_soften_colors">Harmonizuj kolory kalendarza</string> <string name="settings_soften_colors">Zredukuj intensywność kolorów kalendarza</string>
<string name="settings_soften_colors_summary">Zachowaj odcień każdego kalendarza, ale wyrównaj jego jasność, tak aby każde wydarzenie pozostało czytelne, a kolory współgrały ze sobą. Wyłącz tę opcję, aby wyświetlać oryginalne kolory ze źródła kalendarza.</string> <string name="settings_soften_colors_summary">Dopasuj intensywność kolorów kalendarza i wydarzeń do motywu. Wyłącz tę opcję, aby wyświetlać oryginalne kolory ze źródła kalendarza.</string>
<string name="settings_agenda_show_today">Zawsze pokazuj dzisiejszy dzień</string> <string name="settings_agenda_show_today">Zawsze pokazuj dzisiejszy dzień</string>
<string name="settings_agenda_show_today_hint">Zachowaj dzisiejszy dzień na górze agendy i widżetu, nawet gdy nie ma już na dziś żadnych zadań.</string> <string name="settings_agenda_show_today_hint">Zachowaj dzisiejszy dzień na górze agendy i widżetu, nawet gdy nie ma już na dziś żadnych zadań.</string>
<string name="special_dates_calendar_birthday">Urodziny</string> <string name="special_dates_calendar_birthday">Urodziny</string>
@@ -543,128 +543,4 @@
<string name="month_split_no_events">Brak planów</string> <string name="month_split_no_events">Brak planów</string>
<string name="month_split_expand">Pokaż cały miesiąc</string> <string name="month_split_expand">Pokaż cały miesiąc</string>
<string name="month_split_collapse">Pokaż wydarzenia dnia</string> <string name="month_split_collapse">Pokaż wydarzenia dnia</string>
<string name="event_move_gone">To wydarzenie już nie istnieje</string>
<string name="event_move_action">Przenieś…</string>
<string name="event_move_recurring_title">Przenieś wydarzenie cykliczne</string>
<string name="event_move_occurrence_only">Przeniesienie całej serii zmieniłoby dni, w których wypadają wydarzenia, więc można przenieść tylko to wystąpienie.</string>
<string name="event_move_done">Przeniesiono na %1$s</string>
<string name="event_move_undo">Cofnij</string>
<string name="event_move_undone">Cofnięto przeniesienie</string>
<string name="event_move_failed">Nie udało się przenieść wydarzenia</string>
<string name="event_move_write_denied">Calendula wymaga uprawnień do zapisu, aby móc przenosić wydarzenia</string>
<string name="event_move_blocked_series_end">Nie można przenieść wydarzenia poza datę zakończenia jego serii</string>
<string name="event_edit_recurrence_next">Następne: %1$s</string>
<string name="event_edit_recurrence_next_none">Ta reguła nie ma powtórzeń</string>
<string name="event_access_default_summary">Standardowe ustawienie tego kalendarza</string>
<string name="event_access_public_summary">Każdy, kto ma dostęp, widzi pełne szczegóły</string>
<string name="event_access_private_summary">Inni widzą tylko to, że jesteś zajęty</string>
<string name="event_access_confidential_summary">Oznaczone jako poufne; co to oznacza, zależy od konta kalendarza</string>
<string name="duration_hours_minutes">%1$s %2$s</string>
<string name="duration_custom_max">Maksymalnie %1$s</string>
<string name="onboarding_step_counter">Krok %1$d z %2$d</string>
<string name="onboarding_backup_title">Twoje wydarzenia istnieją tylko tutaj</string>
<string name="onboarding_backup_body">Nic z tego telefonu nie synchronizuje się z kontem, więc jego utrata oznaczałaby utratę kalendarza. Calendula może utworzyć dla Ciebie kopię zapasową.</string>
<string name="onboarding_backup_benefit_folder_title">Wybrany przez Ciebie folder</string>
<string name="onboarding_backup_benefit_folder_body">Kopie zapasowe to zwykłe pliki .ics — umieść je w miejscu, które się synchronizuje, albo na karcie SD.</string>
<string name="onboarding_backup_benefit_daily_title">Raz dziennie, automatycznie</string>
<string name="onboarding_backup_benefit_daily_body">Calendula eksportuje Twoje lokalne kalendarze w tle. Możesz zmienić częstotliwość w Ustawieniach.</string>
<string name="onboarding_backup_enable_button">Wybierz folder i utwórz kopię zapasową</string>
<string name="onboarding_view_title">Co powinno się otworzyć jako pierwsze?</string>
<string name="onboarding_month_style_title">Jak powinien wyglądać miesiąc?</string>
<string name="onboarding_view_continue_button">Kontynuuj</string>
<string name="onboarding_back">Wstecz</string>
<string name="onboarding_visibility_button">Rozumiem</string>
<string name="onboarding_done_title">Wszystko gotowe</string>
<string name="onboarding_done_body">Twoje kalendarze są gotowe. Wszystko co właśnie wybrałeś możesz później zmienić w Ustawieniach.</string>
<string name="onboarding_done_button">Otwórz mój kalendarz</string>
<plurals name="search_selected_count">
<item quantity="one">%d wybrane</item>
<item quantity="few">%d wybrane</item>
<item quantity="many">%d wybranych</item>
<item quantity="other">%d wybranych</item>
</plurals>
<string name="search_in_descriptions">Znaleziono w opisach</string>
<string name="search_selection_close">Anuluj wybór</string>
<string name="search_select_all">Wybierz wszystkie</string>
<string name="search_delete_selected">Usuń wybrane</string>
<plurals name="search_delete_title">
<item quantity="one">Usunąć %d wydarzenie?</item>
<item quantity="few">Usunąć %d wydarzenia?</item>
<item quantity="many">Usunąć %d wydarzeń?</item>
<item quantity="other">Usunąć %d wydarzeń?</item>
</plurals>
<plurals name="search_delete_done">
<item quantity="one">Usunięto %d wydarzenie</item>
<item quantity="few">Usunięto %d wydarzenia</item>
<item quantity="many">Usunięto %d wydarzeń</item>
<item quantity="other">Usunięto %d wydarzeń</item>
</plurals>
<string name="search_delete_partial">%1$d usunięto, %2$d nie udało się</string>
<string name="search_delete_denied_partial">%1$d usunięto, następnie utracono uprawnienia do zapisu</string>
<string name="settings_theme_hint">Czy aplikacja ma być jasna, czy ciemna. Wybór zostanie zastosowany natychmiast.</string>
<string name="settings_theme_system_summary">Obecnie %1$s</string>
<string name="settings_font_specimen_heading">czwartek, 14 maja</string>
<string name="settings_font_specimen_body">Przegląd zespołu o 10:00, a potem lunch z Robinem w kawiarni na rynku.</string>
<string name="settings_week_start_auto_summary">Obecnie %1$s</string>
<string name="settings_time_format_auto_summary">Zgodnie z systemem: %1$s</string>
<string name="settings_timeline_scale">Wysokość godzin</string>
<string name="settings_timeline_scale_hint">Ile miejsca w pionie zajmuje jedna godzina w widoku tygodnia i dnia. Oba widoki korzystają z tego ustawienia. Możesz również rozciągnąć lub ścisnąć oś czasu dwoma palcami, aby ustawić dowolną wysokość pomiędzy.</string>
<string name="timeline_scale_fit_day">Cały dzień na ekranie</string>
<string name="timeline_scale_fit_day_summary">Cała doba na jednym ekranie, bez przewijania</string>
<string name="timeline_scale_compact">Kompaktowa</string>
<string name="timeline_scale_compact_summary">Więcej godzin na ekranie, mniejsze bloki</string>
<string name="timeline_scale_regular">Standardowa</string>
<string name="timeline_scale_regular_summary">Standardowy rozmiar</string>
<string name="timeline_scale_comfortable">Wygodna</string>
<string name="timeline_scale_comfortable_summary">Większe bloki, więcej przewijania</string>
<string name="timeline_scale_custom">Własna</string>
<string name="timeline_scale_custom_summary">Wysokość, którą wybrałeś gestem „uszczypnięcia” osi czasu</string>
<string name="settings_drag_to_reschedule">Przeciągnij, aby zmienić termin</string>
<string name="settings_drag_to_reschedule_summary">Przenieś wydarzenie, przeciągając je na inny dzień lub godzinę</string>
<string name="settings_past_events_sample_morning">Przegląd zespołu</string>
<string name="settings_past_events_sample_midday">Lunch z Robin</string>
<string name="settings_past_events_sample_evening">Próba chóru</string>
<string name="settings_widget_size">Rozmiar widżetu agendy</string>
<string name="settings_widget_size_hint">Rozmiar tekstu wyświetlanego przez widżet agendy na ekranie domowym. Widżet miesiąca nie posiada tego ustawienia — jego siatka zawsze dopasowuje się do przydzielonego miejsca.</string>
<string name="settings_widget_size_small">Mały</string>
<string name="settings_widget_size_medium">Średni</string>
<string name="settings_widget_size_large">Duży</string>
<string name="settings_widget_size_extra_large">Bardzo duży</string>
<string name="settings_widget_size_summary">Tekst wydarzenia %1$d%%</string>
<string name="settings_event_duration">Domyślny czas trwania</string>
<string name="settings_event_duration_hint">Czas trwania nowego wydarzenia do momentu zmiany godziny jego zakończenia. Nie dotyczy to wydarzeń całodniowych.</string>
<string name="settings_calendar_durations_title">Czas trwania dla poszczególnych kalendarzy</string>
<string name="settings_calendar_durations_hint">Przypisz kalendarzowi własną domyślną długość — np. 8 godzin dla zmian w pracy.</string>
<string name="settings_calendar_duration_inherits">Domyślny (%1$s)</string>
<string name="settings_calendar_duration_use_default">Używaj domyślnego czasu trwania (%1$s)</string>
<string name="settings_allday_reminder_fires_at">Powiadamia o %1$s</string>
<string name="settings_group_look">Wygląd i zachowanie</string>
<string name="settings_group_data">Dane</string>
<string name="settings_group_app">Aplikacja</string>
<string name="settings_group_about">O aplikacji</string>
<string name="settings_widgets_subtitle">Widżet agendy, kafelek Szybkich ustawień</string>
<string name="settings_backup_subtitle">Eksport, import, automatyczna kopia zapasowa</string>
<string name="settings_section_widgets">Widżety i kafelki</string>
<string name="settings_widgets_hint">Ustawienia widżetów ekranu głównego i kafelka Szybkich ustawień. Aby dodać widżet, naciśnij i przytrzymaj na ekranie domowym.</string>
<string name="settings_section_backup">Kopia zapasowa i przywracanie</string>
<string name="settings_views_all_header">Wszystkie widoki</string>
<string name="settings_week_day_header">Tydzień i dzień</string>
<string name="settings_default_view_hint">Widok, który pojawia się po uruchomieniu aplikacji Calendula.</string>
<string name="settings_week_start_hint">Dzień, od którego zaczyna się każdy tydzień we wszystkich widokach i widżetach.</string>
<string name="settings_time_format_hint">Format godziny w aplikacji. Opcja automatyczna dostosowuje się do ustawień systemowych.</string>
<string name="settings_past_events_hint">Co agenda robi z wydarzeniami, które już się zakończyły.</string>
<string name="settings_dynamic_color_summary">Dopasuj kolory aplikacji do swojej tapety.</string>
<string name="settings_about_privacy">Polityka prywatności</string>
<string name="calendars_visibility_hint">Wyłącz kalendarz, aby ukryć go na tym urządzeniu — jego wydarzenia znikną z aplikacji i przestaniesz otrzymywać powiadomienia. To ten sam przełącznik, którego używają inne aplikacje kalendarza, więc one również go ukryją. Nic nie zostanie usunięte, nie wpłynie to na inne urządzenia, a kalendarz możesz tu włączyć ponownie w dowolnym momencie.</string>
<string name="calendars_visibility_a11y">Pokaż \"%1$s\"</string>
<string name="calendars_visibility_notice_title">Niektóre kalendarze są wyłączone</string>
<string name="calendars_visibility_notice_message">Calendula wyświetla kalendarze włączone na tym urządzeniu, a niektóre z Twoich są wyłączone. Aby zobaczyć ich wydarzenia, włącz je w sekcji Ustawienia → Kalendarze.</string>
<string name="calendar_picker_missing_title">Brakuje jakiegoś kalendarza?</string>
<string name="calendar_picker_missing_summary">Może być wyłączony, tylko do odczytu lub wypełniony na podstawie Twoich kontaktów — zarządzaj swoimi kalendarzami tutaj.</string>
<string name="calendars_state_read_only">Tylko do odczytu</string>
<string name="calendars_state_not_synced">Niezsynchronizowany z tym urządzeniem</string>
<string name="calendars_state_managed">Wypełniony na podstawie Twoich kontaktów</string>
<string name="calendars_managed_delete_locked">Ten kalendarz jest uzupełniany na podstawie Twoich kontaktów, więc Calendula utworzy go ponownie przy następnej synchronizacji. Aby go usunąć, wyłącz daty szczególne w menu Ustawienia → Szczególne daty kontaktów.</string>
<string name="calendars_account_from_source">%1$s (%2$s)</string>
<string name="onboarding_backup_skip_button">Nie teraz</string>
</resources> </resources>

View File

@@ -6,33 +6,34 @@
<item>Days</item> <item>Days</item>
<item>Weeks</item> <item>Weeks</item>
</string-array> </string-array>
<string name="app_tagline">现代化的日历应用</string> <string name="app_name">Calendula</string>
<string name="app_tagline">一个现代化的日历应用</string>
<string name="state_loading">加载中…</string> <string name="state_loading">加载中…</string>
<string name="state_retry">重试</string> <string name="state_retry">推迟</string>
<string name="state_failure_unknown">出错了。</string> <string name="state_failure_unknown">错误!</string>
<string name="state_failure_permission">需要日历访问权限</string> <string name="state_failure_permission">需要日历权限</string>
<string name="state_failure_permission_action">授予访问权限</string> <string name="state_failure_permission_action">授予访问权限</string>
<string name="state_failure_no_calendars">未配置任何日历</string> <string name="state_failure_no_calendars">无已设置的日历</string>
<string name="state_failure_no_calendars_action">打开系统日历设置</string> <string name="state_failure_no_calendars_action">打开系统日历设置</string>
<string name="state_failure_provider">无法读取日历</string> <string name="state_failure_provider">无法读取日历</string>
<string name="permission_rationale_title">优雅地查看您所有的事件</string> <string name="permission_rationale_title">优雅地查看你的所有活动</string>
<string name="permission_rationale_body">Calendula 需要访问您的日历,以展示和管理您的事件</string> <string name="permission_rationale_body">Calendula需要获取权限来访问您的日历展示和管理您的活动。所有权限均会在使用前申请——不必担心隐私泄露</string>
<string name="permission_request_button">授予日历访问权限</string> <string name="permission_request_button">授予日历权限</string>
<string name="permission_denied_title">日历访问被拒绝</string> <string name="permission_denied_title">日历权限不可用</string>
<string name="permission_denied_body">Calendula 无法没有日历访问权限的情况下显示事件。可以在系统设置中重新授予权限</string> <string name="permission_denied_body">Calendula 无法显示没有日历访问权限的事件。可以在系统设置里再次授予它</string>
<string name="permission_open_settings_button">打开系统设置</string> <string name="permission_open_settings_button">打开系统设置</string>
<string name="permission_retry_button">重试</string> <string name="permission_retry_button">重试</string>
<string name="permission_benefit_private_title">从设计上保护隐私</string> <string name="permission_benefit_private_title">保留在设备</string>
<string name="permission_benefit_private_body">没有网络权限,没有遥测——任何数据都不会离开您的手机</string> <string name="permission_benefit_private_body">您的日历数据将会在本地读取,而不会被上传</string>
<string name="permission_benefit_sync_title">所有日历汇聚一处</string> <string name="permission_benefit_sync_title">所有日历</string>
<string name="permission_benefit_sync_body">Google、CalDAV、本地日历——只要同步到设备就会自动显示</string> <string name="permission_benefit_sync_body">Google、CalDAV、本地设备——任何同步到设备的东西都会直接显示出来</string>
<string name="permission_benefit_privacy_title">无追踪</string> <string name="permission_benefit_privacy_title">无追踪</string>
<string name="permission_benefit_privacy_body">零遥测零分析、无广告。</string> <string name="permission_benefit_privacy_body">零遥测零分析,没有广告。</string>
<string name="permission_privacy_footnote">数据保留在您的设备 · 无网络权限</string> <string name="permission_privacy_footnote">数据保留在本地设备 ·无网络权限</string>
<string name="month_prev">上月</string> <string name="month_prev"></string>
<string name="month_next">下月</string> <string name="month_next"></string>
<string name="month_today_action">今天</string> <string name="month_today_action">今天</string>
<string name="month_more_actions">更多操作</string> <string name="month_more_actions">更多选项</string>
<string name="month_open_menu">打开菜单</string> <string name="month_open_menu">打开菜单</string>
<string name="month_action_settings">设置</string> <string name="month_action_settings">设置</string>
<string name="month_a11y_today_prefix">今天</string> <string name="month_a11y_today_prefix">今天</string>
@@ -42,572 +43,80 @@
<string name="event_detail_edit">编辑</string> <string name="event_detail_edit">编辑</string>
<string name="event_detail_delete">删除</string> <string name="event_detail_delete">删除</string>
<string name="event_detail_share">分享</string> <string name="event_detail_share">分享</string>
<string name="event_share_chooser_title">分享事件</string> <string name="event_share_chooser_title">分享活动</string>
<string name="event_share_failed">无法分享此事件</string> <string name="event_share_failed">无法分享该活动</string>
<string name="event_delete_title">删除事件</string> <string name="event_delete_title">删除活动</string>
<string name="event_delete_body">事件将从的日历以及所有同步设备中移除。</string> <string name="event_delete_body">活动将从的日历以及所有同步设备中移除。</string>
<string name="event_delete_recurring_title">删除重复事件</string> <string name="event_delete_recurring_title">删除重复活动</string>
<string name="event_delete_option_occurrence">此事件</string> <string name="event_delete_option_occurrence">该活动</string>
<string name="event_delete_option_following">此事件及其后的所有事件</string> <string name="event_delete_option_following">当前及接下来的所有活动</string>
<string name="event_delete_option_series">整个系列的所有事件</string> <string name="event_delete_option_series">系列的所有活动</string>
<string name="event_edit_recurring_title">编辑重复事件</string> <string name="event_edit_recurring_title">编辑重复活动</string>
<string name="event_delete_failed">无法删除事件</string> <string name="event_delete_failed">无法删除该活动</string>
<string name="event_delete_write_denied">Calendula 需要写入权限才能删除事件</string> <string name="event_delete_write_denied">Calendula需要权限读取或删除活动</string>
<string name="dialog_cancel">取消</string> <string name="dialog_cancel">取消</string>
<string name="dialog_ok"></string> <string name="dialog_ok"></string>
<string name="event_edit_new_title">事件</string> <string name="event_edit_new_title">活动</string>
<string name="event_edit_close">关闭</string> <string name="event_edit_close">关闭</string>
<string name="event_edit_save">保存</string> <string name="event_edit_save">保存</string>
<string name="event_edit_title_hint">加标题</string> <string name="event_edit_title_hint">加标题</string>
<string name="event_edit_managed_hint">由“%1$s”管理——标题、日期和重复会从您的联系人同步。提醒、位置和备注由您编辑。</string> <string name="event_edit_managed_hint">由“%1$s”管理——标题、日期和重复内容会与你的联系人保持同步。提醒、位置和笔记由你自行编辑。</string>
<string name="event_edit_starts">开始</string> <string name="event_edit_starts">开始</string>
<string name="event_edit_ends">结束</string> <string name="event_edit_ends">结束</string>
<string name="event_edit_error_end_before_start">结束时间早于始时间</string> <string name="event_edit_error_end_before_start">报错:结束时间早于始时间</string>
<string name="event_edit_error_no_calendar">没有可写入的日历</string> <string name="event_edit_error_no_calendar">可写入的日历</string>
<string name="event_edit_save_failed">无法保存事件</string> <string name="event_edit_save_failed">无法保存该活动</string>
<string name="event_edit_write_denied">Calendula 需要写入权限才能创建事件</string> <string name="event_edit_write_denied">Calendula需要写入权限来创建活动</string>
<string name="event_edit_more_fields">更多字段</string> <string name="event_edit_more_fields">其他领域</string>
<string name="event_edit_add">添加</string> <string name="event_edit_add">新增</string>
<string name="event_edit_add_reminder">添加提醒</string> <string name="event_edit_add_reminder">新建提醒</string>
<string name="event_edit_remove_reminder">移除提醒</string> <string name="event_edit_remove_reminder">移除提醒</string>
<string name="event_edit_attendees">参与</string> <string name="event_edit_attendees">访问</string>
<string name="event_edit_add_guest">添加参与</string> <string name="event_edit_add_guest">新增访问</string>
<string name="event_edit_add_guest_hint">通过电子邮件添加参与者…</string> <string name="event_edit_add_guest_hint">通过 Email 新增一个访问者…</string>
<string name="event_edit_add_guest_from_contacts">从联系人添加</string> <string name="event_edit_add_guest_from_contacts">从联系人添加访问者</string>
<string name="event_edit_location_from_contacts">从联系人选取地址</string> <string name="event_edit_location_from_contacts">从联系人中添加地址</string>
<string name="event_edit_remove_guest">移除参与</string> <string name="event_edit_remove_guest">移除访问</string>
<string name="event_edit_attendee_required"></string> <string name="event_edit_attendee_required"></string>
<string name="event_edit_attendee_optional">可选</string> <string name="event_edit_attendee_optional">可选</string>
<string name="event_edit_attendees_note_synced">Calendula 不发送邀请。您的日历户同步时可能会通过邮件通知参与者</string> <string name="event_edit_attendees_note_synced">Calendula 不发送邀请函。你的日历户同步时可能会给访客发邮件</string>
<string name="event_edit_attendees_note_local">存储于本设备,不会通知任何人</string> <string name="event_edit_attendees_note_local">保存在本设备,其他访客不会被提醒</string>
<string name="event_edit_reminder_custom">自定义</string> <string name="event_edit_reminder_custom">自定义</string>
<string name="reminder_unit_minutes">分钟</string> <string name="reminder_unit_minutes">分钟</string>
<string name="reminder_unit_hours">小时</string> <string name="reminder_unit_hours">小时</string>
<string name="reminder_unit_days"></string> <string name="reminder_unit_days"></string>
<string name="reminder_unit_weeks"></string> <string name="reminder_unit_weeks"></string>
<string name="event_edit_availability">忙闲状态</string> <string name="event_edit_availability">空闲</string>
<string name="event_edit_visibility">可见性</string> <string name="event_edit_visibility">隐私设置</string>
<string name="event_edit_color"></string> <string name="event_edit_color"></string>
<string name="event_edit_color_default">日历颜色</string> <string name="event_edit_color_default">日历颜色</string>
<string name="event_edit_color_custom">自定义颜色</string> <string name="event_edit_color_custom">自定义颜色</string>
<string name="event_edit_color_reset">重置</string> <string name="event_edit_color_reset">重置</string>
<string name="event_edit_color_unsupported">日历不支持</string> <string name="event_edit_color_unsupported">日历不可用</string>
<string name="event_edit_color_unsupported_hint">此日历未提供颜色集。您可以在“设置”中为此类日历允许自定义颜色。</string> <string name="event_edit_color_unsupported_hint">此日历未设置任何颜色集。您可以在“设置”中为此类日历启用自定义颜色。</string>
<string name="event_edit_color_sync_warning">日历次同步时可能会丢弃或覆盖该颜色。</string> <string name="event_edit_color_sync_warning">日历在下一次同步时可能会删除或覆盖该颜色。</string>
<string name="event_edit_conflict_title">事件已在别处更改</string> <string name="event_edit_conflict_title">其他设备的活动已更改</string>
<string name="event_edit_conflict_body">您编辑时,事件已通过同步或其他应用修改。您的更改应如何处理?</string> <string name="event_edit_conflict_body">编辑时,事件已同步或其他应用修改。您的修改要怎么处理?</string>
<string name="event_edit_conflict_overwrite">保存我的更改</string> <string name="event_edit_conflict_overwrite">保存我的更改</string>
<string name="event_edit_conflict_overwrite_hint">覆盖您编辑的字段</string> <string name="event_edit_conflict_overwrite_hint">仅您编辑的字段会覆盖外部更改</string>
<string name="event_edit_conflict_discard">丢弃我的更改</string> <string name="event_edit_conflict_discard">丢弃我的更改</string>
<string name="event_edit_conflict_discard_hint">事件保持当前状态</string> <string name="event_edit_conflict_discard_hint">该活动保持现状</string>
<string name="event_edit_gone_title">事件已被删除</string> <string name="event_edit_gone_title">活动被删除</string>
<string name="event_edit_gone_body">事件在您编辑期间已被删除(例如在另一设备上)。您的更改无法保存。</string> <string name="event_edit_gone_body">这个事件在您编辑期间已被删除了(比如在另一设备上)。您的修改现在无法保存。</string>
<string name="event_edit_recurrence_none">重复</string> <string name="event_edit_recurrence_none">确认无需重复</string>
<string name="event_edit_recurrence_custom">自定义</string> <string name="event_edit_recurrence_custom">自定义</string>
<string name="event_edit_recurrence_every"></string> <string name="event_edit_recurrence_every"></string>
<string name="recurrence_unit_days"></string> <string name="recurrence_unit_days"></string>
<string name="recurrence_unit_weeks"></string> <string name="recurrence_unit_weeks"></string>
<string name="recurrence_unit_months"></string> <string name="recurrence_unit_months"></string>
<string name="recurrence_unit_years"></string> <string name="recurrence_unit_years"></string>
<string name="event_edit_recurrence_ends">结束</string> <string name="event_edit_recurrence_ends">结束时间</string>
<string name="event_edit_recurrence_end_never"></string> <string name="event_edit_recurrence_end_never"></string>
<string name="event_edit_recurrence_end_until">于指定日期</string> <string name="event_edit_recurrence_end_until">于指定日期</string>
<string name="event_edit_recurrence_end_count">在指定次数</string> <string name="event_edit_recurrence_end_count">重复几次</string>
<string name="event_edit_recurrence_times"></string> <string name="event_edit_recurrence_times"></string>
<string name="event_edit_error_recurrence_ends_before_start">重复结束日期早于事件开始时间</string> <string name="event_edit_error_recurrence_ends_before_start">报错:结束日期早于开始日期</string>
<string name="event_availability_busy">忙碌</string> <string name="event_availability_busy">占用中</string>
<string name="event_access_default">默认</string> <string name="event_access_default">默认</string>
<string name="event_access_public">公开</string> <string name="event_access_public">公开</string>
<string name="event_detail_duplicate">复制</string>
<string name="event_move_action">移动…</string>
<string name="event_move_recurring_title">删除重复活动</string>
<string name="event_move_occurrence_only">移动整个系列会改变其发生日期,因此只能移动此事件。</string>
<string name="event_move_done">已移动到 %1$s</string>
<string name="event_move_undo">撤销</string>
<string name="event_move_undone">已撤销移动</string>
<string name="event_move_failed">无法保存该活动</string>
<string name="event_move_write_denied">Calendula需要权限读取或删除活动</string>
<string name="event_move_gone">该事件已不存在</string>
<string name="event_move_blocked_series_end">无法将事件移至其系列结束日期之后</string>
<string name="event_edit_timezone_device">设备时区</string>
<string name="event_edit_timezone_device_summary">跟随您所在位置</string>
<string name="event_edit_timezone_search">搜索时区</string>
<string name="event_edit_timezone_recent">最近使用</string>
<string name="event_edit_timezone_all">所有时区</string>
<string name="event_edit_timezone_none">没有与“%1$s”匹配的时区</string>
<string name="event_edit_timezone_local_time">%1$s 您的本地时间</string>
<string name="import_reminder_prompt_title">应用默认提醒?</string>
<string name="import_reminder_prompt_body_none">此事件导入时没有提醒。</string>
<plurals name="import_reminder_prompt_body_existing">
<item quantity="other">此事件导入时包含 %1$d 个提醒。</item>
</plurals>
<string name="app_name">Calendula</string>
<string name="week_number_label"></string>
<string name="import_reminder_prompt_apply">应用默认</string>
<string name="import_reminder_prompt_keep">保留原样</string>
<string name="event_edit_recurrence_incomplete">请输入 1 到 999 之间的数字</string>
<string name="event_edit_recurrence_next">下一次:%1$s</string>
<string name="event_edit_recurrence_next_none">此规则永不重复</string>
<string name="event_detail_all_day">全天</string>
<string name="event_detail_calendar">Calendula</string>
<string name="event_detail_calendar_unknown">未知日历</string>
<string name="event_detail_location">地点</string>
<string name="event_detail_description">描述</string>
<string name="event_detail_attendees">参与者</string>
<string name="event_detail_recurrence">重复</string>
<string name="event_detail_recurring">重复事件</string>
<string name="recurrence_daily">每天</string>
<string name="recurrence_weekly">每周</string>
<string name="recurrence_monthly">每月</string>
<string name="recurrence_yearly">每年</string>
<string name="recurrence_every_n_days">每 %1$d 天</string>
<string name="recurrence_every_n_weeks">每 %1$d 周</string>
<string name="recurrence_every_n_months">每 %1$d 月</string>
<string name="recurrence_every_n_years">每 %1$d 年</string>
<string name="recurrence_on_days">%1$s%2$s</string>
<string name="recurrence_with_until">%1$s直到 %2$s</string>
<string name="recurrence_with_count">%1$s共 %2$d 次</string>
<string name="event_detail_not_found">此事件已不存在。</string>
<string name="event_attendee_accepted">已接受</string>
<string name="event_attendee_declined">已拒绝</string>
<string name="event_attendee_tentative">待定</string>
<string name="event_attendee_needs_action">未回复</string>
<string name="event_attendee_unknown"></string>
<string name="event_detail_reminders">提醒</string>
<string name="event_detail_timezone">时区</string>
<string name="event_status_tentative">待定</string>
<string name="event_status_cancelled">取消</string>
<string name="event_availability_free">空闲</string>
<string name="event_access_private">私密</string>
<string name="event_access_confidential">机密</string>
<string name="event_access_default_summary">按日历默认设置</string>
<string name="event_access_public_summary">有权限者可见全部详情</string>
<string name="event_access_private_summary">他人仅看到您忙碌</string>
<string name="event_access_confidential_summary">标记为机密,具体含义由日历帐户决定</string>
<string name="event_attendee_organizer">组织者</string>
<string name="event_attendee_optional">可选项</string>
<string name="event_attendee_resource">资源</string>
<string name="event_detail_self_response">您的回复:%1$s</string>
<string name="reminder_at_time">事件开始时</string>
<string name="reminder_default">默认提醒</string>
<plurals name="reminder_minutes">
<item quantity="other">提前 %d 分钟</item>
</plurals>
<plurals name="reminder_hours">
<item quantity="other">提前 %d 小时</item>
</plurals>
<plurals name="reminder_days">
<item quantity="other">提前 %d 天</item>
</plurals>
<plurals name="reminder_weeks">
<item quantity="other">提前 %d 周</item>
</plurals>
<plurals name="duration_minutes">
<item quantity="other">%d分钟</item>
</plurals>
<plurals name="duration_hours">
<item quantity="other">%d 小时</item>
</plurals>
<plurals name="duration_days">
<item quantity="other">%d 天</item>
</plurals>
<string name="duration_hours_minutes">%1$s %2$s</string>
<string name="duration_custom_max">最多 %1$s</string>
<plurals name="duration_weeks">
<item quantity="other">%d 周</item>
</plurals>
<string name="event_untitled">(无标题)</string>
<string name="onboarding_step_counter">第 %1$d 步,共 %2$d 步</string>
<string name="onboarding_backup_title">您的事件仅保存在这里</string>
<string name="onboarding_backup_body">本手机上的事件不会同步到任何帐户丢失手机将丢失日历。Calendula 可以为您创建备份。</string>
<string name="onboarding_backup_benefit_folder_title">您选择的文件夹</string>
<string name="onboarding_backup_benefit_folder_body">备份为纯 .ics 文件——可存放在同步文件夹或 SD 卡中。</string>
<string name="onboarding_backup_benefit_daily_title">每天自动备份</string>
<string name="onboarding_backup_benefit_daily_body">Calendula 会在后台导出您的本地日历。您可以在“设置”中更改频率。</string>
<string name="onboarding_backup_enable_button">选择文件夹并备份</string>
<string name="onboarding_backup_skip_button">暂不</string>
<string name="onboarding_view_title">默认打开哪个视图?</string>
<string name="onboarding_month_style_title">月视图样式?</string>
<string name="onboarding_view_continue_button">继续</string>
<string name="onboarding_back">返回</string>
<string name="onboarding_visibility_button">知道了</string>
<string name="onboarding_done_title">一切就绪</string>
<string name="onboarding_done_body">您的日历已就绪。所有选择均可在“设置”中更改。</string>
<string name="onboarding_done_button">打开我的日历</string>
<string name="reminder_channel_name">事件提醒</string>
<string name="reminder_channel_description">按事件提醒时间推送的通知</string>
<string name="reminder_onboarding_title">不错过任何事件</string>
<string name="reminder_onboarding_body">Android 本身不会显示事件提醒——需要日历应用来承担。让 Calendula 接管这项工作吧。</string>
<string name="reminder_benefit_delivery_title">准时送达提醒</string>
<string name="reminder_benefit_delivery_body">您事件中的每个提醒都会按时以通知形式送达。</string>
<string name="reminder_benefit_duplicates_title">使用第二个日历应用?</string>
<string name="reminder_benefit_duplicates_body">如果另一个应用也发送提醒,您会看到两次——请在其中一个中关闭。</string>
<string name="reminder_benefit_reversible_title">随时更改</string>
<string name="reminder_benefit_reversible_body">开关位于“设置”中的“通知”下。</string>
<string name="reminder_onboarding_enable_button">开启提醒</string>
<string name="reminder_onboarding_skip_button">暂不</string>
<string name="reminder_action_snooze">稍后提醒</string>
<string name="reminder_action_dismiss">关闭</string>
<string name="reminder_day_tomorrow">明天</string>
<string name="reminder_day_yesterday">昨天</string>
<string name="view_month"></string>
<string name="view_week"></string>
<string name="view_day"></string>
<string name="view_agenda">日程</string>
<string name="view_section">视图</string>
<string name="drawer_jump_to_date">跳转到日期</string>
<string name="agenda_today_action">今天</string>
<string name="agenda_header_today">今天</string>
<string name="agenda_header_tomorrow">明天</string>
<string name="agenda_empty_title">暂无待办</string>
<string name="agenda_no_more_today">今天没有更多事件</string>
<string name="agenda_span_starts">开始 %1$s</string>
<string name="agenda_span_ends">结束 %1$s</string>
<string name="today_jump_action">去今天</string>
<string name="search_action">搜索</string>
<string name="search_hint">搜索事件</string>
<string name="search_back">返回</string>
<string name="search_clear">清除</string>
<string name="search_idle_hint">按标题、地点或备注搜索您的事件。</string>
<string name="search_empty">没有与“%1$s”匹配的事件。</string>
<plurals name="search_selected_count">
<item quantity="other">已选 %d 个</item>
</plurals>
<string name="search_in_descriptions">在描述中找到</string>
<string name="search_selection_close">取消选择</string>
<string name="search_select_all">全选</string>
<string name="search_delete_selected">删除所选</string>
<plurals name="search_delete_title">
<item quantity="other">删除 %d个活动</item>
</plurals>
<plurals name="search_delete_done">
<item quantity="other">删除了%d个活动</item>
</plurals>
<string name="search_delete_partial">已删除 %1$d 个,%2$d 个无法删除</string>
<string name="search_delete_denied_partial">已删除 %1$d 个,之后写入权限被撤</string>
<string name="widget_agenda_title">即将开始</string>
<string name="widget_agenda_label">Calendula 日程</string>
<string name="widget_month_label">Calendula 月视图</string>
<string name="widget_refresh">刷新</string>
<string name="widget_new_event">新活动</string>
<string name="widget_needs_permission">打开 Calendula 授予日历访问权限</string>
<string name="widget_prev_month">上个月</string>
<string name="widget_next_month">下个月</string>
<string name="widget_today">今天</string>
<string name="filter_title">日历</string>
<string name="settings_title">设置</string>
<string name="settings_back">返回</string>
<string name="back">返回</string>
<string name="settings_section_appearance">外观</string>
<string name="settings_theme">主题</string>
<string name="settings_theme_system">跟随系统</string>
<string name="settings_theme_light">浅色</string>
<string name="settings_theme_dark">深色</string>
<string name="settings_theme_hint">应用使用浅色或深色主题。选择后立即生效。</string>
<string name="settings_theme_system_summary">当前为 %1$s</string>
<string name="settings_default_view">默认视图</string>
<string name="settings_dynamic_color">动态颜色</string>
<string name="settings_dynamic_color_unavailable">需要 Android 12 或更高版本</string>
<string name="settings_soften_colors">协调日历颜色</string>
<string name="settings_soften_colors_summary">保留每个日历的色调但均衡亮度,使所有事件清晰可读且色彩协调。关闭则显示日历源的原始颜色。</string>
<string name="settings_font_headings">标题字体</string>
<string name="settings_font_body">正文字体</string>
<string name="settings_font_system">系统默认</string>
<string name="font_atkinson_hyperlegible">Atkinson Hyperlegible</string>
<string name="font_lora">Lora</string>
<string name="font_jetbrains_mono">JetBrains Mono</string>
<string name="settings_font_choose_file">选择文件…</string>
<string name="settings_font_custom_selected">自定义字体</string>
<string name="settings_font_import_failed">无法将文件识别为字体</string>
<string name="settings_font_specimen_heading">2026年8月28日 星期四</string>
<string name="settings_font_specimen_body">团队评审 10:00然后与 Robin 在集市广场咖啡馆共进午餐。</string>
<string name="settings_week_start">每周起始日</string>
<string name="settings_week_start_auto">自动</string>
<string name="settings_week_start_auto_summary">当前为 %1$s</string>
<string name="settings_week_numbers">周数</string>
<string name="settings_week_numbers_summary">在月视图中显示日历周数</string>
<string name="settings_today_toolbar">工具栏中的“今天”按钮</string>
<string name="settings_today_toolbar_summary">在工具栏中显示“跳至今天”按钮,而非浮动按钮</string>
<string name="settings_app_name">应用名称</string>
<string name="settings_app_name_summary">在启动器中显示为“日历”。仅启动器名称改变;切换后图标可能移动到新位置。</string>
<string name="settings_time_format">时间格式</string>
<string name="settings_time_format_auto">自动</string>
<string name="settings_time_format_12h">12 小时制(下午 2:00</string>
<string name="settings_time_format_24h">24 小时制14:00</string>
<string name="settings_time_format_auto_summary">跟随系统:%1$s</string>
<string name="settings_hour_lines">整点分隔线</string>
<string name="settings_hour_lines_summary">在周视图和日视图中显示每个整点的分隔线</string>
<string name="settings_timeline_scale">小时高度</string>
<string name="settings_timeline_scale_hint">周视图和日视图中每小时占用的垂直空间。两者共享此设置。您也可以用双指捏合时间线设置任意高度。</string>
<string name="timeline_scale_fit_day">适应全天</string>
<string name="timeline_scale_fit_day_summary">24 小时一屏显示,无需滚动</string>
<string name="timeline_scale_compact">紧凑</string>
<string name="timeline_scale_compact_summary">每屏显示更多小时,区块更小</string>
<string name="timeline_scale_regular">常规</string>
<string name="timeline_scale_regular_summary">标准间距</string>
<string name="timeline_scale_comfortable">舒适</string>
<string name="timeline_scale_comfortable_summary">区块更宽敞,需更多滚动</string>
<string name="timeline_scale_custom">自定义</string>
<string name="timeline_scale_custom_summary">您捏合时间线所设定的高度</string>
<string name="settings_dim_completed">变暗已完成事件</string>
<string name="settings_dim_completed_summary">在月视图和周视图中淡化已结束的事件</string>
<string name="settings_drag_to_reschedule">拖拽重新安排</string>
<string name="settings_drag_to_reschedule_summary">通过拖拽事件到另一天或时间来移动它</string>
<string name="settings_past_events">过去事件</string>
<string name="settings_past_events_show">显示</string>
<string name="settings_past_events_dim">变暗</string>
<string name="settings_past_events_hide">隐藏</string>
<string name="settings_past_events_sample_morning">团队评审</string>
<string name="settings_past_events_sample_midday">与 Robin 午餐</string>
<string name="settings_past_events_sample_evening">合唱团练习</string>
<string name="settings_agenda_header">日程</string>
<string name="settings_agenda_range">日程范围</string>
<string name="settings_agenda_range_hint">“日程”视图列出未来事件的时间跨度。</string>
<string name="settings_agenda_widget_range">日程小部件范围</string>
<string name="settings_agenda_widget_range_hint">主屏幕日程小部件列出未来事件的时间跨度。</string>
<string name="settings_widget_size">日程小部件大小</string>
<string name="settings_widget_size_hint">主屏幕日程小部件绘制文字的大小。月视图小部件无此设置——它的网格始终适应您给定的空间。</string>
<string name="settings_widget_size_small"></string>
<string name="settings_widget_size_medium"></string>
<string name="settings_widget_size_large"></string>
<string name="settings_widget_size_extra_large">特大</string>
<string name="settings_widget_size_summary">事件文字 %1$d%%</string>
<string name="settings_agenda_show_today">始终显示今天</string>
<string name="settings_agenda_show_today_hint">即使今天没有剩余事件,也将今天保持在日程及其小部件顶部。</string>
<string name="settings_agenda_range_bar">范围栏</string>
<string name="settings_agenda_range_bar_hint">在日程顶部显示一个条,标明显示的日期范围,并提供一个按钮用于临时切换范围</string>
<string name="agenda_range_day">今天</string>
<string name="agenda_range_this_week">本周</string>
<string name="agenda_range_this_month">本月</string>
<string name="agenda_range_week">未来 7 天</string>
<string name="agenda_range_month">未来 30 天</string>
<string name="agenda_range_custom">自定义…</string>
<string name="agenda_range_custom_hint">天数</string>
<string name="agenda_range_override_hint">仅本次生效——重新打开 Calendula 时恢复为已保存的范围。</string>
<string name="agenda_range_showing_label">显示所有未来事件</string>
<plurals name="agenda_range_days">
<item quantity="other">%d 天</item>
</plurals>
<string name="settings_section_views">视图</string>
<string name="settings_month_header">月视图</string>
<string name="settings_month_view_style">月视图样式</string>
<string name="month_style_paged">分页</string>
<string name="month_style_paged_summary">一屏显示一个月,左右滑动切换月份。</string>
<string name="month_style_continuous">滚动月份</string>
<string name="month_style_continuous_summary">每个月在其标题下,与下个月之间略有间距。</string>
<string name="month_style_dense">无缝周</string>
<string name="month_style_dense_summary">各周连续排列,月份之间无间隙。</string>
<string name="month_style_split">拆分</string>
<string name="month_style_split_summary">紧凑网格标记有事件的日子,点击的日期在下方列出。</string>
<string name="month_split_no_events">无安排</string>
<string name="month_split_expand">显示整月</string>
<string name="month_split_collapse">显示当天事件</string>
<string name="settings_quick_switch_header">快速切换按钮</string>
<string name="settings_quick_switch_hint">选择右上角按钮循环切换的视图,并拖动排序。关闭的视图仍可从导航菜单访问。</string>
<string name="settings_drawer_order_header">导航菜单</string>
<string name="settings_drawer_order_hint">拖动排序导航菜单中的视图。</string>
<string name="reorder_drag_handle">拖动以排序</string>
<string name="settings_section_event_form">新建事件表单</string>
<string name="settings_form_fields_hint">默认显示的字段——其他字段位于“更多字段”下</string>
<string name="settings_autofocus_title">新建事件时聚焦标题</string>
<string name="settings_autofocus_title_hint">创建新事件时,光标自动置于标题字段并弹出键盘。</string>
<string name="settings_event_duration">默认时长</string>
<string name="settings_event_duration_hint">新事件在您更改结束时间之前的默认时长。全天事件不受影响。</string>
<string name="settings_calendar_durations_title">按日历设置时长</string>
<string name="settings_calendar_durations_hint">为某个日历单独设置默认时长——例如工作班次设为 8 小时。</string>
<string name="settings_calendar_duration_inherits">默认(%1$s</string>
<string name="settings_calendar_duration_use_default">使用默认时长(%1$s</string>
<string name="settings_color_unsupported">允许在不支持的日历上使用颜色</string>
<string name="settings_color_unsupported_hint">某些日历(例如某些 CalDAV未发布颜色集自定义事件颜色可能会在下一次同步时被丢弃或覆盖。这是那些日历的限制并非 Calendula 可修复的问题。</string>
<string name="settings_section_notifications">通知</string>
<string name="settings_reminders">事件提醒</string>
<string name="settings_reminders_hint">看到重复提醒?另一个日历应用也在发送——请关闭其中一个。</string>
<string name="settings_default_reminder">默认提醒</string>
<string name="settings_default_reminder_allday">全天事件</string>
<string name="settings_allday_reminder_time">全天事件提醒时间</string>
<string name="settings_allday_reminder_time_hint">全天事件的提醒在 %1$s 触发</string>
<string name="settings_allday_reminder_fires_at">在 %1$s 通知</string>
<string name="reminder_none"></string>
<string name="reminder_use_default">使用默认提醒</string>
<string name="reminder_custom_amount">数量</string>
<string name="reminder_custom_set">设置</string>
<string name="settings_calendar_reminders_title">按日历设置提醒</string>
<string name="settings_calendar_reminders_hint">为每个日历覆盖默认提醒——分别针对定时事件和全天事件。日历可保持默认、不使用提醒,或设置自己的提醒。</string>
<string name="settings_calendar_reminder_inherits">默认(%1$s</string>
<string name="settings_reliable_delivery">可靠送达</string>
<string name="settings_reliable_delivery_hint">Android 可能为省电延迟提醒。豁免 Calendula 以使其准时送达。</string>
<string name="settings_reliable_delivery_exempt">豁免电池优化——提醒准时送达。</string>
<string name="settings_snooze_duration">贪睡时长</string>
<string name="settings_section_calendars">日历</string>
<string name="settings_manage_calendars">管理日历</string>
<string name="settings_manage_calendars_hint">创建本地日历;管理同步日历</string>
<string name="settings_section_language">语言</string>
<string name="settings_language">应用语言</string>
<string name="settings_language_auto">系统默认</string>
<string name="settings_translate">帮助翻译</string>
<string name="settings_translate_hint">在 Weblate 上添加或改进语言</string>
<string name="settings_group_look">外观与行为</string>
<string name="settings_group_data">数据</string>
<string name="settings_group_app">应用</string>
<string name="settings_group_about">关于</string>
<string name="settings_appearance_subtitle">主题、颜色、字体</string>
<string name="settings_views_subtitle">默认视图、布局、顺序</string>
<string name="settings_event_form_subtitle">默认字段和行为</string>
<string name="settings_notifications_subtitle">提醒与送达</string>
<string name="settings_special_dates_subtitle">联系人生日和纪念日</string>
<string name="settings_widgets_subtitle">日程小部件、快速设置磁贴</string>
<string name="settings_backup_subtitle">导出、导入、自动备份</string>
<string name="settings_section_widgets">小部件与磁贴</string>
<string name="settings_widgets_hint">主屏幕小部件和快速设置磁贴的设置。长按主屏幕即可添加小部件。</string>
<string name="settings_section_backup">备份与恢复</string>
<string name="settings_views_all_header">所有视图</string>
<string name="settings_week_day_header">周与日</string>
<string name="settings_default_view_hint">启动 Calendula 时默认打开的视图。</string>
<string name="settings_week_start_hint">所有视图和小部件中每周开始的日子。</string>
<string name="settings_time_format_hint">整个应用的时间显示格式。自动跟随系统设置。</string>
<string name="settings_past_events_hint">日程对已结束事件的处理方式。</string>
<string name="settings_dynamic_color_summary">从壁纸中提取应用颜色。</string>
<string name="settings_section_special_dates">联系人特殊日期</string>
<string name="settings_special_dates_enable">显示联系人日期</string>
<string name="settings_special_dates_enable_hint">将联系人生日和其他日期镜像到本地日历。仅读取本设备上的联系人——不会上传任何内容,也不会修改您的联系人。</string>
<string name="settings_special_dates_type_birthday">生日</string>
<string name="settings_special_dates_type_anniversary">纪念日</string>
<string name="settings_special_dates_type_custom">其他日期</string>
<string name="settings_special_dates_template">标题格式</string>
<string name="settings_special_dates_template_hint">使用 {name} 表示联系人,{year} 表示年份(出生年份或纪念日开始年份;未知时隐藏)。</string>
<string name="settings_special_dates_reminders">提醒</string>
<string name="settings_special_dates_show_year">显示年份</string>
<string name="settings_special_dates_show_year_hint">在标题中包含 {year}(当已知时)</string>
<string name="settings_special_dates_sync_now">立即同步</string>
<string name="settings_special_dates_never_synced">尚未同步</string>
<string name="settings_special_dates_last_synced">上次同步:%1$s</string>
<string name="settings_special_dates_calendar_hint">在“日历”设置中分别设置每个日历的颜色和可见性。</string>
<string name="settings_calendar_reminders_managed_hint">在“联系人特殊日期”中设置</string>
<string name="settings_special_dates_paused_title">已暂停</string>
<string name="settings_special_dates_paused_hint">Calendula 无法读取您的联系人,因此这些日历未更新。</string>
<string name="settings_special_dates_grant">授予访问权限</string>
<string name="settings_special_dates_disable_title">关闭联系人日期?</string>
<string name="settings_special_dates_disable_all_message">这将删除联系人日期日历及其事件。您添加的任何提醒或备注将丢失。</string>
<string name="settings_special_dates_disable_type_message">这将删除“%1$s”日历及其事件。您添加的任何提醒或备注将丢失。</string>
<string name="settings_special_dates_disable_confirm">关闭</string>
<string name="dialog_save">保存</string>
<string name="settings_section_about">关于</string>
<string name="settings_license">许可协议</string>
<string name="settings_license_value">MIT</string>
<string name="settings_about_author">作者 Jean-Luc Makiola</string>
<string name="settings_about_source">源代码</string>
<string name="settings_about_privacy">隐私政策</string>
<string name="settings_about_support">支持开发</string>
<string name="settings_about_version">版本 %1$s</string>
<string name="settings_about_logo_desc">Calendula 应用图标</string>
<string name="settings_report_problem">报告问题</string>
<string name="settings_report_problem_hint">发送崩溃报告或打开问题追踪器</string>
<string name="calendars_title">日历</string>
<string name="calendars_local_header">您的日历</string>
<string name="calendars_local_empty">尚未创建本地日历。创建一个以仅在本设备保存事件。</string>
<string name="calendars_add">添加日历</string>
<string name="calendars_visibility_hint">关闭日历可隐藏其在设备上的显示——事件会从应用中消失,同时停止提醒。此开关与其他日历应用共用,因此它们也会隐藏。不会删除任何内容,不会影响其他设备,您随时可在此重新开启。</string>
<string name="calendars_visibility_a11y">显示“%1$s”</string>
<string name="calendars_visibility_notice_title">部分日历已关闭</string>
<string name="calendars_visibility_notice_message">Calendula 显示本设备已开启的日历,但您有一些是关闭的。要查看事件,请在“设置”→“日历”中将其开启。</string>
<string name="calendar_picker_missing_title">找不到日历?</string>
<string name="calendar_picker_missing_summary">可能已关闭、只读,或由联系人填充——在此管理您的日历。</string>
<string name="calendars_state_read_only">只读</string>
<string name="calendars_state_not_synced">未同步到本设备</string>
<string name="calendars_state_managed">由联系人填充</string>
<string name="calendars_managed_delete_locked">此日历由联系人填充Calendula 下次同步时会重新创建。要删除它,请在“设置”→“特殊日期”中关闭相关功能。</string>
<string name="calendars_synced_header">同步日历</string>
<string name="calendars_synced_hint">来自设备上的帐户。请在对应的应用中创建和编辑。</string>
<string name="calendars_manage_in_app">在应用中管理</string>
<string name="calendars_account_from_source">%1$s%2$s</string>
<string name="calendars_account_menu_a11y">%1$s 的更多选项</string>
<string name="calendars_enable_all">启用全部</string>
<string name="calendars_disable_all">禁用全部</string>
<string name="calendars_add_account">添加帐户</string>
<string name="calendars_new_title">新建日历</string>
<string name="calendars_edit_title">编辑日历</string>
<string name="calendars_name_label">名称</string>
<string name="calendars_color_label">色彩</string>
<string name="calendars_description_hint">添加描述</string>
<string name="calendars_delete_confirm_title">删除日历?</string>
<string name="calendars_delete_confirm_message">“%1$s”及其所有事件将从本设备永久删除。</string>
<string name="calendars_write_error">无法保存更改。</string>
<string name="calendars_backup_header">返回</string>
<string name="calendars_backup_hint">本地日历不会同步到任何地方,请导出为 .ics 文件以保留副本。</string>
<string name="calendars_backup_action">导出为 .ics 文件</string>
<string name="calendars_export_title">导出日历</string>
<string name="calendars_export_hint">选择要包含在 .ics 文件中的日历。</string>
<string name="calendars_export_action">导出</string>
<string name="calendars_restore_header">恢复</string>
<string name="calendars_restore_action">从 .ics 文件恢复</string>
<string name="calendars_restore_hint">从备份或其他日历应用导入事件。</string>
<string name="calendars_auto_backup">自动备份</string>
<string name="calendars_auto_backup_hint">定期将本地日历导出为 .ics 文件保存到文件夹。</string>
<string name="calendars_auto_backup_folder">备份文件夹</string>
<string name="calendars_auto_backup_folder_unset">点击选择文件夹</string>
<string name="calendars_auto_backup_interval">间隔</string>
<string name="calendars_auto_backup_every">每 %1$s</string>
<string name="calendars_auto_backup_interval_min">最短 30 分钟。</string>
<string name="calendars_auto_backup_status_never">尚未自动备份</string>
<string name="calendars_auto_backup_status_ok">上次备份:%1$s</string>
<string name="calendars_auto_backup_status_failed">上次备份失败:%1$s</string>
<string name="backup_channel_name">备份</string>
<string name="backup_channel_description">自动备份反复失败时发出警告。</string>
<string name="backup_failed_title">自动备份失败</string>
<string name="backup_failed_text">Calendula 无法写入备份文件。请在“设置”中检查备份文件夹。</string>
<string name="calendars_backup_failed">无法导出备份。</string>
<plurals name="calendars_backup_done">
<item quantity="other">已导出 %d 个事件。</item>
</plurals>
<string name="import_title">导入事件</string>
<string name="import_target_header">添加至日历</string>
<string name="import_empty">此文件中未找到事件。</string>
<string name="import_failed">无法读取此文件。</string>
<string name="import_no_calendar">没有可写入的日历用于导入。请先创建本地日历。</string>
<string name="import_done_title">导入完成</string>
<string name="import_done_dedup_note">已跳过日历中已存在的事件。</string>
<string name="import_done_added_label">已添加</string>
<string name="import_done_skipped_label">重复项</string>
<string name="import_close">关闭</string>
<string name="import_warning_recurrence">已跳过重复事件中部分已修改的发生项。</string>
<string name="import_warning_no_start">已跳过无开始时间的事件。</string>
<string name="import_warning_attendees">未导入参与者列表。</string>
<string name="import_warning_timezone">未知时区已回退为设备时区。</string>
<string name="import_button">导入</string>
<plurals name="import_title_count">
<item quantity="other">正在导入 %d 个事件</item>
</plurals>
<plurals name="import_event_count">
<item quantity="other">此文件中包含 %d 个事件。</item>
</plurals>
<plurals name="import_action">
<item quantity="other">导入 %d 个事件</item>
</plurals>
<plurals name="import_done_imported">
<item quantity="other">已导入 %d 个事件。</item>
</plurals>
<plurals name="import_done_skipped">
<item quantity="other">已跳过 %d 个(已存在于该日历)。</item>
</plurals>
<string name="shortcut_new_event_short">新事件</string>
<string name="shortcut_new_event_long">创建新事件</string>
<string name="qs_tile_new_event_label">新事件</string>
<string name="settings_qs_tile">添加快速设置磁贴</string>
<string name="settings_qs_tile_hint">在快速设置面板中添加“新事件”磁贴。</string>
<string name="crash_dialog_title">%1$s 已崩溃</string>
<string name="crash_dialog_message">%1$s 上次意外关闭。您可以将此报告作为问题发送以帮助修复。报告在您选择共享之前仅保留在设备上,不含任何个人数据或日历内容——仅包含以下技术细节。</string>
<string name="crash_dialog_report">报告</string>
<string name="crash_dialog_dismiss">暂不</string>
<string name="crash_report_issue_title">崩溃报告</string>
<string name="crash_report_clip_label">%1$s 崩溃报告</string>
<string name="crash_report_copied">报告已复制到剪贴板</string>
<string name="crash_report_open_failed">无法打开问题追踪器。报告已在剪贴板中。</string>
<string name="crash_report_body_template">感谢您报告 %1$s 的崩溃。请补充您记得的操作内容,然后提交。\n\n### 发生了什么\n\n\n### 崩溃报告\n%2$s\n</string>
<string name="crash_report_body_paste">_报告太长无法通过此链接附带——请从剪贴板粘贴。_</string>
<string name="special_dates_calendar_birthday">生日</string>
<string name="special_dates_calendar_anniversary">纪念日</string>
<string name="special_dates_calendar_custom">特殊日期</string>
<string name="special_dates_default_title_birthday">{name} 的生日({year}</string>
<string name="special_dates_default_title_anniversary">{name} 的纪念日({year}</string>
<string name="special_dates_default_title_custom">{name}</string>
</resources> </resources>

View File

@@ -558,6 +558,174 @@ class EventWriteMapperTest {
assertThat(values[CalendarContract.Events.ALL_DAY]).isEqualTo(1) assertThat(values[CalendarContract.Events.ALL_DAY]).isEqualTo(1)
} }
// --- toDetachedOccurrence ("edit only this event", no _sync_id) ---
@Test
fun `a detached occurrence drops the series rule and becomes a one-off row`() {
val edited = form().copy(title = "Moved", rrule = "FREQ=WEEKLY;BYDAY=TH")
val detached = edited.toDetachedOccurrence()
val values = buildEventInsertValues(
form = detached,
uid = "uid@calendula",
times = detached.toWriteTimes(berlin),
)
assertThat(values[CalendarContract.Events.TITLE]).isEqualTo("Moved")
// A surviving rule would insert a second *series* overlapping the first
// (Codeberg #234's stray duplicate).
assertThat(values).doesNotContainKey(CalendarContract.Events.RRULE)
assertThat(values).doesNotContainKey(CalendarContract.Events.DURATION)
// A one-off row carries DTEND rather than a duration.
assertThat(values[CalendarContract.Events.DTSTART]).isEqualTo(1_781_164_800_000L)
assertThat(values[CalendarContract.Events.DTEND]).isEqualTo(1_781_170_200_000L)
assertThat(values[CalendarContract.Events.EVENT_TIMEZONE]).isEqualTo("Europe/Berlin")
}
@Test
fun `a detached occurrence carries every edited field onto the new row`() {
// Built from the form, not cloned from the parent: a field dropped here
// is an edit silently lost.
val edited = form(timezone = "America/New_York").copy(
title = " Standup ",
location = "Room 2",
description = "notes",
reminders = listOf(10),
availability = Availability.Free,
accessLevel = AccessLevel.Private,
rrule = "FREQ=DAILY",
)
val detached = edited.toDetachedOccurrence()
val values = buildEventInsertValues(
form = detached,
uid = "uid@calendula",
times = detached.toWriteTimes(berlin),
)
assertThat(values[CalendarContract.Events.TITLE]).isEqualTo("Standup")
assertThat(values[CalendarContract.Events.EVENT_LOCATION]).isEqualTo("Room 2")
assertThat(values[CalendarContract.Events.DESCRIPTION]).isEqualTo("notes")
assertThat(values[CalendarContract.Events.AVAILABILITY])
.isEqualTo(CalendarContract.Events.AVAILABILITY_FREE)
assertThat(values[CalendarContract.Events.ACCESS_LEVEL])
.isEqualTo(CalendarContract.Events.ACCESS_PRIVATE)
// The pinned zone survives — never re-anchored to the device.
assertThat(values[CalendarContract.Events.EVENT_TIMEZONE]).isEqualTo("America/New_York")
assertThat(values[CalendarContract.Events.UID_2445]).isEqualTo("uid@calendula")
// Reminders aren't columns; the insert path seeds them from the form.
assertThat(detached.reminders).containsExactly(10)
}
@Test
fun `a detached all-day occurrence stays on UTC midnights`() {
val edited = form(
isAllDay = true,
start = LocalDateTime(LocalDate(2026, 6, 11), LocalTime(0, 0)),
end = LocalDateTime(LocalDate(2026, 6, 11), LocalTime(0, 0)),
).copy(title = "Birthday", rrule = "FREQ=YEARLY")
val detached = edited.toDetachedOccurrence()
val values = buildEventInsertValues(
form = detached,
uid = "uid@calendula",
times = detached.toWriteTimes(berlin),
)
assertThat(values[CalendarContract.Events.ALL_DAY]).isEqualTo(1)
assertThat(values[CalendarContract.Events.EVENT_TIMEZONE]).isEqualTo("UTC")
assertThat(values[CalendarContract.Events.DTSTART]).isEqualTo(1_781_136_000_000L)
// Exclusive DTEND — the next UTC midnight.
assertThat(values[CalendarContract.Events.DTEND]).isEqualTo(1_781_222_400_000L)
assertThat(values).doesNotContainKey(CalendarContract.Events.RRULE)
}
@Test
fun `the detached row lands exactly where the parent's exdate removes it`() {
// The two halves must agree on the instant, or the user sees the
// occurrence twice or not at all.
val edited = form().copy(title = "Renamed", rrule = "FREQ=WEEKLY")
val occurrenceMillis = 1_781_164_800_000L
val parent = buildOccurrenceExdateValues(
existingExdate = null,
occurrenceMillis = occurrenceMillis,
dtStartMillis = 1_780_560_000_000L,
rrule = "FREQ=WEEKLY",
duration = "P5400S",
timezone = "Europe/Berlin",
allDay = 0,
)
val detached = edited.toDetachedOccurrence()
val inserted = buildEventInsertValues(
form = detached,
uid = "uid@calendula",
times = detached.toWriteTimes(berlin),
)
assertThat(parent[CalendarContract.Events.EXDATE]).isEqualTo("20260611T080000Z")
assertThat(inserted[CalendarContract.Events.DTSTART]).isEqualTo(occurrenceMillis)
// The parent keeps its own anchor and rule — only this occurrence leaves.
assertThat(parent[CalendarContract.Events.DTSTART]).isEqualTo(1_780_560_000_000L)
assertThat(parent[CalendarContract.Events.RRULE]).isEqualTo("FREQ=WEEKLY")
}
@Test
fun `a detached all-day occurrence matches its date-only exdate stamp`() {
val parent = buildOccurrenceExdateValues(
existingExdate = null,
occurrenceMillis = 1_781_136_000_000L, // 2026-06-11T00:00:00Z
dtStartMillis = 1_749_600_000_000L,
rrule = "FREQ=YEARLY",
duration = "P1D",
timezone = "UTC",
allDay = 1,
)
val detached = form(
isAllDay = true,
start = LocalDateTime(LocalDate(2026, 6, 11), LocalTime(0, 0)),
end = LocalDateTime(LocalDate(2026, 6, 11), LocalTime(0, 0)),
).copy(rrule = "FREQ=YEARLY").toDetachedOccurrence()
val inserted = buildEventInsertValues(
form = detached,
uid = "uid@calendula",
times = detached.toWriteTimes(berlin),
)
assertThat(parent[CalendarContract.Events.EXDATE]).isEqualTo("20260611")
assertThat(inserted[CalendarContract.Events.DTSTART]).isEqualTo(1_781_136_000_000L)
}
// --- exdateContains (guards a second detach of the same occurrence) ---
@Test
fun `an occurrence already excluded is recognised, timed and all-day`() {
// Detaching twice would leave a second standalone copy.
assertThat(
exdateContains("20260611T080000Z", 1_781_164_800_000L, isAllDay = false),
).isTrue()
assertThat(
exdateContains("20260611", 1_781_136_000_000L, isAllDay = true),
).isTrue()
}
@Test
fun `an occurrence not in the exdate list is not mistaken for an excluded one`() {
assertThat(exdateContains(null, 1_781_164_800_000L, isAllDay = false)).isFalse()
assertThat(exdateContains("", 1_781_164_800_000L, isAllDay = false)).isFalse()
// A neighbouring occurrence must not match — the guard is per-instant.
assertThat(
exdateContains("20260610T080000Z", 1_781_164_800_000L, isAllDay = false),
).isFalse()
}
@Test
fun `an exclusion is found anywhere in a multi-entry exdate list`() {
// Whitespace after a comma is legal in the stored column.
assertThat(
exdateContains(
"20260604T080000Z, 20260611T080000Z,20260618T080000Z",
1_781_164_800_000L,
isAllDay = false,
),
).isTrue()
}
// --- per-event colour --- // --- per-event colour ---
@Test @Test

View File

@@ -26,7 +26,7 @@ lifecycleCompose = "2.10.0"
androidxTestRules = "1.7.0" androidxTestRules = "1.7.0"
# Glance: 1.1.1 is the latest stable (1.2.0 is still rc, 1.3.0 alpha). # Glance: 1.1.1 is the latest stable (1.2.0 is still rc, 1.3.0 alpha).
glance = "1.1.1" glance = "1.1.1"
work = "2.11.2" work = "2.10.1"
documentfile = "1.1.0" documentfile = "1.1.0"
[libraries] [libraries]