Merge main into release/v2.19.4
Brings #248 (EXDATE moves with the series when its times change) onto the release branch, alongside the translations and dependency updates that landed on main. #245 and #248 both grew EventWriteMapper's EXDATE handling in the same place, which conflicted. They are independent — #245 added exdateContains for the detach path, #248 added shiftedExdate/exdateAfter for re-anchoring — so both sides are kept, and their tests with them.
This commit is contained in:
11
CHANGELOG.md
11
CHANGELOG.md
@@ -8,6 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- **A deleted occurrence no longer comes back when the series is re-timed.**
|
||||
Delete a single occurrence of a repeating event, then change the time of the
|
||||
whole series, and the occurrence you had removed reappeared. Doing it the
|
||||
other way round — "This and all following events" — lost every removal in the
|
||||
part of the series being changed. Removals now travel with the event: they
|
||||
keep their place in the series across a time change, a move to a different
|
||||
day, a timezone change and a switch to or from an all-day event, and they
|
||||
carry over when a series is split. Repeating events on your device's own
|
||||
calendars are affected, including the birthday and anniversary calendars
|
||||
Calendula creates from your contacts ([#248]).
|
||||
- **Home-screen widgets now turn the page at midnight.** Both the month and the
|
||||
agenda widget kept highlighting yesterday as "today" — and the agenda kept
|
||||
greying out the wrong events as already past — until you paged the month back
|
||||
@@ -1522,3 +1532,4 @@ automatically, with zero telemetry and no internet permission.
|
||||
[#225]: https://codeberg.org/jlmakiola/calendula/issues/225
|
||||
[#228]: https://codeberg.org/jlmakiola/calendula/issues/228
|
||||
[#234]: https://codeberg.org/jlmakiola/calendula/issues/234
|
||||
[#248]: https://codeberg.org/jlmakiola/calendula/issues/248
|
||||
|
||||
@@ -1005,10 +1005,12 @@ class AndroidCalendarDataSource @Inject constructor(
|
||||
updated: EventForm,
|
||||
allDayReminderTimeMinutes: Int,
|
||||
) {
|
||||
val row = querySeriesRow(eventId)
|
||||
val values = buildEventUpdateValues(
|
||||
original = original,
|
||||
updated = updated,
|
||||
seriesDtStartMillis = querySeriesRow(eventId).dtStartMillis,
|
||||
seriesDtStartMillis = row.dtStartMillis,
|
||||
seriesExdate = row.exdate,
|
||||
zone = ZoneId.systemDefault(),
|
||||
)
|
||||
if (values.isNotEmpty()) {
|
||||
@@ -1347,10 +1349,66 @@ class AndroidCalendarDataSource @Inject constructor(
|
||||
}
|
||||
// Insert the new series first: if it fails, the original is untouched.
|
||||
val newEventId = insertEvent(updated, allDayReminderTimeMinutes)
|
||||
carrySplitExdate(newEventId, row, beginMillis, original, updated)
|
||||
truncateSeries(eventId, row, beginMillis)
|
||||
return newEventId
|
||||
}
|
||||
|
||||
/**
|
||||
* Carry the [parent]'s exclusions for occurrences past [beginMillis] onto the
|
||||
* series [newEventId] that now owns them, re-timed by the shift the split
|
||||
* applied ([shiftedExdate]/[exdateAfter]). [insertEvent] builds the new row
|
||||
* from the form, which knows nothing about them, so without this every
|
||||
* occurrence the user had deleted from the tail of the series comes back.
|
||||
*
|
||||
* The whole time/recurrence set rides along with EXDATE for the reason
|
||||
* [buildOccurrenceExdateValues] documents: on its own the provider does not
|
||||
* read an EXDATE write as a recurrence change, and leaves the instances it
|
||||
* expanded on insert standing.
|
||||
*
|
||||
* A failure rolls the new series back and throws, so the split fails whole
|
||||
* rather than landing with the exclusions quietly dropped — the parent is
|
||||
* still untruncated at this point, so the event is left exactly as it was.
|
||||
*/
|
||||
private fun carrySplitExdate(
|
||||
newEventId: Long,
|
||||
parent: SeriesRow,
|
||||
beginMillis: Long,
|
||||
original: EventForm,
|
||||
updated: EventForm,
|
||||
) {
|
||||
// Dropping the recurrence in the same save leaves a one-off tail, and an
|
||||
// exclusion means nothing on a row that doesn't recur.
|
||||
if (updated.rrule.isNullOrBlank()) return
|
||||
val carried = shiftedExdate(
|
||||
existingExdate = exdateAfter(parent.exdate, beginMillis, original.isAllDay),
|
||||
original = original,
|
||||
updated = updated,
|
||||
zone = ZoneId.systemDefault(),
|
||||
) ?: return
|
||||
val row = querySeriesRow(newEventId)
|
||||
val values = ContentValues().apply {
|
||||
put(CalendarContract.Events.EXDATE, carried)
|
||||
put(CalendarContract.Events.DTSTART, row.dtStartMillis)
|
||||
put(CalendarContract.Events.RRULE, row.rrule)
|
||||
put(CalendarContract.Events.DURATION, row.duration)
|
||||
put(CalendarContract.Events.EVENT_TIMEZONE, row.timezone)
|
||||
put(CalendarContract.Events.ALL_DAY, row.allDay)
|
||||
}
|
||||
try {
|
||||
val rows = resolver.update(
|
||||
ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, newEventId),
|
||||
values, null, null,
|
||||
)
|
||||
if (rows == 0) {
|
||||
throw WriteFailedException("carry exdate onto split series id=$newEventId")
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
runCatching { deleteEvent(newEventId) }
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteEventFromOccurrence(eventId: Long, beginMillis: Long) {
|
||||
val row = querySeriesRow(eventId)
|
||||
// From the first occurrence on = the whole series; also the fallback
|
||||
|
||||
@@ -14,6 +14,9 @@ import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.ResolverStyle
|
||||
import java.time.LocalDate as JavaLocalDate
|
||||
import java.time.LocalDateTime as JavaLocalDateTime
|
||||
|
||||
/** Provider-ready DTSTART / DTEND / EVENT_TIMEZONE for an event write. */
|
||||
@@ -138,18 +141,21 @@ internal fun buildEventInsertValues(
|
||||
*
|
||||
* Time fields travel together (the provider validates them as a unit):
|
||||
* - unchanged times, all-day flag and rrule → no time columns at all;
|
||||
* - non-recurring result → DTSTART/DTEND, DURATION and RRULE cleared;
|
||||
* - non-recurring result → DTSTART/DTEND, DURATION, RRULE and EXDATE cleared;
|
||||
* - recurring result → the *series* DTSTART moves by the same **wall-clock**
|
||||
* shift the user applied to the displayed occurrence and is re-resolved in the
|
||||
* event's zone ([seriesDtStartMillis] is the row's current DTSTART), DURATION
|
||||
* replaces DTEND, RRULE is written. This keeps past occurrences intact when
|
||||
* someone edits a later occurrence's time, and keeps the anchor's time-of-day
|
||||
* stable across a DST boundary or a zone change between the two.
|
||||
* replaces DTEND, RRULE is written, and the row's exclusions
|
||||
* ([seriesExdate], the current EXDATE) travel with the anchor. This keeps past
|
||||
* occurrences intact when someone edits a later occurrence's time, and keeps
|
||||
* the anchor's time-of-day stable across a DST boundary or a zone change
|
||||
* between the two.
|
||||
*/
|
||||
internal fun buildEventUpdateValues(
|
||||
original: EventForm,
|
||||
updated: EventForm,
|
||||
seriesDtStartMillis: Long,
|
||||
seriesExdate: String?,
|
||||
zone: ZoneId,
|
||||
): Map<String, Any?> = buildMap {
|
||||
if (updated.title.trim() != original.title.trim()) {
|
||||
@@ -189,6 +195,10 @@ internal fun buildEventUpdateValues(
|
||||
put(CalendarContract.Events.DTEND, newTimes.dtEndMillis)
|
||||
put(CalendarContract.Events.RRULE, null)
|
||||
put(CalendarContract.Events.DURATION, null)
|
||||
// The exclusions named occurrences of a series that no longer exists.
|
||||
// Left behind they are dormant rather than harmless: adding a recurrence
|
||||
// back later would punch the old holes into the new one.
|
||||
put(CalendarContract.Events.EXDATE, null)
|
||||
} else {
|
||||
// Move the series anchor by the *wall-clock* shift the user applied to the
|
||||
// displayed occurrence, then re-resolve it in the event's (possibly new)
|
||||
@@ -210,6 +220,14 @@ internal fun buildEventUpdateValues(
|
||||
put(CalendarContract.Events.DTEND, null)
|
||||
put(CalendarContract.Events.RRULE, updated.rrule)
|
||||
put(CalendarContract.Events.DURATION, newTimes.toRfc2445Duration(updated.isAllDay))
|
||||
// An EXDATE stamp is an absolute instant, so it excludes an occurrence
|
||||
// only while the series keeps generating one at exactly that instant. The
|
||||
// anchor has just moved, so every occurrence regenerates elsewhere and a
|
||||
// stamp left behind matches none of them — the occurrence the user deleted
|
||||
// comes back. Move the stamps the same way the anchor moved.
|
||||
shiftedExdate(seriesExdate, original, updated, zone)
|
||||
?.takeIf { it != seriesExdate }
|
||||
?.let { put(CalendarContract.Events.EXDATE, it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,11 +457,7 @@ internal fun buildOccurrenceExdateValues(
|
||||
allDay: Int,
|
||||
): Map<String, Any?> {
|
||||
val stamp = formatExdateStamp(occurrenceMillis, isAllDay = allDay != 0)
|
||||
val existing = existingExdate?.split(',')
|
||||
?.map { it.trim() }
|
||||
?.filter { it.isNotEmpty() }
|
||||
.orEmpty()
|
||||
val merged = (existing + stamp).distinct().joinToString(",")
|
||||
val merged = (exdateStamps(existingExdate) + stamp).distinct().joinToString(",")
|
||||
return mapOf(
|
||||
CalendarContract.Events.EXDATE to merged,
|
||||
CalendarContract.Events.DTSTART to dtStartMillis,
|
||||
@@ -471,22 +485,122 @@ internal fun exdateContains(
|
||||
return existingExdate?.split(',')?.any { it.trim() == stamp } == true
|
||||
}
|
||||
|
||||
/**
|
||||
* [existingExdate] with every stamp re-timed by the same **wall-clock** shift the
|
||||
* series anchor takes from [original] to [updated] — the exclusions' half of the
|
||||
* anchor move [buildEventUpdateValues] performs, and what carries them onto the
|
||||
* new series when "this and following" splits one.
|
||||
*
|
||||
* A stamp is an absolute instant, so it keeps naming its occurrence only if it
|
||||
* moves exactly as the occurrence does: shifted in wall clock and re-resolved in
|
||||
* the event's (possibly new) zone. A millisecond delta would instead bake in the
|
||||
* offset that happened to apply at the edited occurrence — an hour off for any
|
||||
* exclusion on the far side of a DST boundary. All-day-ness is read from
|
||||
* [original] and written from [updated], so the `VALUE=DATE` and date-time forms
|
||||
* convert into each other when the event switches.
|
||||
*
|
||||
* Null when there is nothing to carry: no stamps, or a stamp in a form Calendula
|
||||
* never writes (a `TZID=`-parameterised or floating one from a sync adapter). Those
|
||||
* are left exactly as they are rather than guessed at — a stale stamp excludes
|
||||
* nothing, but a mangled one could exclude the wrong occurrence.
|
||||
*/
|
||||
internal fun shiftedExdate(
|
||||
existingExdate: String?,
|
||||
original: EventForm,
|
||||
updated: EventForm,
|
||||
zone: ZoneId,
|
||||
): String? {
|
||||
val stamps = exdateStamps(existingExdate)
|
||||
if (stamps.isEmpty()) return null
|
||||
val fromZone = original.writeZone(zone)
|
||||
val toZone = updated.writeZone(zone)
|
||||
val wallClockShift = Duration.between(original.anchorLocal(), updated.anchorLocal())
|
||||
return stamps
|
||||
.map { stamp ->
|
||||
val local = parseExdateStamp(stamp, original.isAllDay, fromZone) ?: return null
|
||||
formatExdateStamp(local.plus(wallClockShift), updated.isAllDay, toZone)
|
||||
}
|
||||
.distinct()
|
||||
.joinToString(",")
|
||||
}
|
||||
|
||||
/**
|
||||
* The stamps of [existingExdate] naming occurrences after [beginMillis] — the ones
|
||||
* that belong to the *new* series once "this and following" splits a recurring
|
||||
* event there. The parent keeps the full list; its stamps past the split point are
|
||||
* simply inert once it stops generating those occurrences.
|
||||
*
|
||||
* The occurrence at [beginMillis] itself is never carried. It is the one the user
|
||||
* is editing, so it exists by definition, and honouring a stale exclusion for it
|
||||
* would swallow the edit whole.
|
||||
*
|
||||
* Null when nothing qualifies, or when a stamp can't be read (see [shiftedExdate]).
|
||||
*/
|
||||
internal fun exdateAfter(existingExdate: String?, beginMillis: Long, isAllDay: Boolean): String? {
|
||||
val stamps = exdateStamps(existingExdate)
|
||||
if (stamps.isEmpty()) return null
|
||||
return stamps
|
||||
.filter { stamp ->
|
||||
val utc = parseExdateStamp(stamp, isAllDay, ZoneOffset.UTC) ?: return null
|
||||
utc.toInstant(ZoneOffset.UTC).toEpochMilli() > beginMillis
|
||||
}
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.joinToString(",")
|
||||
}
|
||||
|
||||
/** The individual stamps of an EXDATE column value; it is a comma-separated list. */
|
||||
private fun exdateStamps(exdate: String?): List<String> = exdate
|
||||
?.split(',')
|
||||
?.map { it.trim() }
|
||||
?.filter { it.isNotEmpty() }
|
||||
.orEmpty()
|
||||
|
||||
/**
|
||||
* 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
|
||||
* reads off the UTC calendar day.
|
||||
*/
|
||||
private fun formatExdateStamp(occurrenceMillis: Long, isAllDay: Boolean): String {
|
||||
val utc = Instant.ofEpochMilli(occurrenceMillis).atZone(ZoneOffset.UTC)
|
||||
return if (isAllDay) {
|
||||
"%04d%02d%02d".format(utc.year, utc.monthValue, utc.dayOfMonth)
|
||||
private fun formatExdateStamp(occurrenceMillis: Long, isAllDay: Boolean): String =
|
||||
formatExdateStamp(
|
||||
local = Instant.ofEpochMilli(occurrenceMillis).atZone(ZoneOffset.UTC).toLocalDateTime(),
|
||||
isAllDay = isAllDay,
|
||||
zone = ZoneOffset.UTC,
|
||||
)
|
||||
|
||||
/**
|
||||
* One EXDATE entry for the occurrence whose wall clock in [zone] is [local]. An
|
||||
* all-day entry keeps only the date (its time-of-day is the anchor's, not the
|
||||
* occurrence's); a timed one is resolved in [zone] and written as a UTC instant.
|
||||
*/
|
||||
private fun formatExdateStamp(local: JavaLocalDateTime, isAllDay: Boolean, zone: ZoneId): String =
|
||||
if (isAllDay) {
|
||||
local.toLocalDate().format(ALL_DAY_EXDATE)
|
||||
} else {
|
||||
"%04d%02d%02dT%02d%02d%02dZ".format(
|
||||
utc.year, utc.monthValue, utc.dayOfMonth,
|
||||
utc.hour, utc.minute, utc.second,
|
||||
)
|
||||
local.atZone(zone).withZoneSameInstant(ZoneOffset.UTC).format(TIMED_EXDATE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [stamp] as the wall clock it names in [zone] — the inverse of
|
||||
* [formatExdateStamp]. Null for anything but the two forms Calendula writes, so a
|
||||
* caller can tell "not ours, leave it alone" from a value it may safely re-time.
|
||||
*/
|
||||
private fun parseExdateStamp(stamp: String, isAllDay: Boolean, zone: ZoneId): JavaLocalDateTime? =
|
||||
runCatching {
|
||||
if (isAllDay) {
|
||||
JavaLocalDate.parse(stamp, ALL_DAY_EXDATE).atStartOfDay()
|
||||
} else {
|
||||
JavaLocalDateTime.parse(stamp, TIMED_EXDATE)
|
||||
.atZone(ZoneOffset.UTC).withZoneSameInstant(zone).toLocalDateTime()
|
||||
}
|
||||
}.getOrNull()
|
||||
|
||||
/** `VALUE=DATE` EXDATE form, for an all-day series. */
|
||||
private val ALL_DAY_EXDATE: DateTimeFormatter =
|
||||
DateTimeFormatter.ofPattern("uuuuMMdd").withResolverStyle(ResolverStyle.STRICT)
|
||||
|
||||
/** UTC date-time EXDATE form, for a timed series. */
|
||||
private val TIMED_EXDATE: DateTimeFormatter =
|
||||
DateTimeFormatter.ofPattern("uuuuMMdd'T'HHmmss'Z'").withResolverStyle(ResolverStyle.STRICT)
|
||||
|
||||
/**
|
||||
* The `EVENT_COLOR` / `EVENT_COLOR_KEY` columns for a colour selection. A
|
||||
|
||||
@@ -43,25 +43,25 @@
|
||||
<string name="day_today_action">Dnes</string>
|
||||
<string name="event_detail_back">Zpět</string>
|
||||
<string name="event_detail_edit">Upravit</string>
|
||||
<string name="event_detail_delete">Vymazat</string>
|
||||
<string name="event_detail_delete">Odstranit</string>
|
||||
<string name="event_detail_share">Sdílet</string>
|
||||
<string name="event_detail_duplicate">Kopírovat</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_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_recurring_title">Smazat opakující se událost</string>
|
||||
<string name="event_delete_recurring_title">Odstranit opakující se 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_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_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 mazat události</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_move_action">Přesunout…</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_done">Přesunuto do %1$s</string>
|
||||
<string name="event_move_undo">Zpět</string>
|
||||
<string name="event_move_undo">Vrátit</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_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_default">Barva kalendáře</string>
|
||||
<string name="event_edit_color_custom">Vlastní barva</string>
|
||||
<string name="event_edit_color_reset">Obnovit</string>
|
||||
<string name="event_edit_color_reset">Znovu nastavit</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_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_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_gone_title">Událost byla smazána</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="event_edit_gone_title">Událost byla odstraně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="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>
|
||||
<plurals name="import_reminder_prompt_body_existing">
|
||||
@@ -250,4 +250,86 @@
|
||||
<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_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>
|
||||
|
||||
@@ -16,14 +16,14 @@
|
||||
<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="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ć. To jedyne, o co prosi na wstępie — i żadne dane nigdy nie opuszczają Twojego urządzenia.</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_request_button">Przyznaj dostęp 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_open_settings_button">Otwórz ustawienia systemowe</string>
|
||||
<string name="permission_retry_button">Spróbuj ponownie</string>
|
||||
<string name="permission_benefit_private_title">Pozostaje na Twoim urządzeniu</string>
|
||||
<string name="permission_benefit_private_body">Twoje kalendarze są odczytywane lokalnie i nigdy nie opuszczają telefonu.</string>
|
||||
<string name="permission_benefit_private_title">Prywatność z założenia</string>
|
||||
<string name="permission_benefit_private_body">Brak uprawnień do internetu, brak telemetrii — nic nigdy nie opuszcza Twojego telefonu.</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_privacy_title">Nigdy żadnego śledzenia</string>
|
||||
@@ -333,7 +333,7 @@
|
||||
<string name="settings_default_reminder">Domyślne przypomnienie</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_hint">Przypomnienia o wydarzeniach całodniowych zostaną pojawiają się o %1$s</string>
|
||||
<string name="settings_allday_reminder_time_hint">Przypomnienia o wydarzeniach całodniowych pojawiają się o %1$s</string>
|
||||
<string name="reminder_none">Brak</string>
|
||||
<string name="reminder_use_default">Użyj domyślnego przypomnienia</string>
|
||||
<string name="reminder_custom_amount">jednostek</string>
|
||||
@@ -353,10 +353,10 @@
|
||||
<string name="settings_language_auto">Domyślny systemu</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_appearance_subtitle">Motyw, domyślny widok, pierwszy dzień tygodnia</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 dla nowych wydarzeń</string>
|
||||
<string name="settings_notifications_subtitle">Przypomnienia o wydarzeniach</string>
|
||||
<string name="settings_appearance_subtitle">Motyw, kolory, czcionki</string>
|
||||
<string name="settings_views_subtitle">Domyślny widok, układ, kolejność</string>
|
||||
<string name="settings_event_form_subtitle">Domyślne pola i zachowanie</string>
|
||||
<string name="settings_notifications_subtitle">Przypomnienia i dostarczanie</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_special_dates_enable">Pokaż daty kontaktów</string>
|
||||
@@ -509,8 +509,8 @@
|
||||
<string name="reminder_day_tomorrow">Jutro</string>
|
||||
<string name="reminder_day_yesterday">Wczoraj</string>
|
||||
<string name="agenda_no_more_today">Brak zaplanowanych wydarzeń</string>
|
||||
<string name="settings_soften_colors">Zredukuj intensywność kolorów 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_soften_colors">Harmonizuj kolory 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_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="special_dates_calendar_birthday">Urodziny</string>
|
||||
@@ -543,4 +543,128 @@
|
||||
<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_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>
|
||||
|
||||
@@ -6,34 +6,33 @@
|
||||
<item>Days</item>
|
||||
<item>Weeks</item>
|
||||
</string-array>
|
||||
<string name="app_name">Calendula</string>
|
||||
<string name="app_tagline">一个现代化的日历应用</string>
|
||||
<string name="app_tagline">现代化的日历应用</string>
|
||||
<string name="state_loading">加载中…</string>
|
||||
<string name="state_retry">推迟</string>
|
||||
<string name="state_failure_unknown">错误!</string>
|
||||
<string name="state_failure_permission">需要日历权限</string>
|
||||
<string name="state_retry">重试</string>
|
||||
<string name="state_failure_unknown">出错了。</string>
|
||||
<string name="state_failure_permission">需要日历访问权限。</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_provider">无法读取日历</string>
|
||||
<string name="permission_rationale_title">优雅地查看你的所有活动</string>
|
||||
<string name="permission_rationale_body">Calendula需要获取权限来访问您的日历来展示和管理您的活动。所有权限均会在使用前申请——不必担心隐私泄露。</string>
|
||||
<string name="permission_request_button">授予日历权限</string>
|
||||
<string name="permission_denied_title">日历权限不可用</string>
|
||||
<string name="permission_denied_body">Calendula 无法显示没有日历访问权限的事件。你可以在系统设置里再次授予它。</string>
|
||||
<string name="state_failure_provider">无法读取日历。</string>
|
||||
<string name="permission_rationale_title">优雅地查看您所有的事件</string>
|
||||
<string name="permission_rationale_body">Calendula 需要访问您的日历,以展示和管理您的事件。</string>
|
||||
<string name="permission_request_button">授予日历访问权限</string>
|
||||
<string name="permission_denied_title">日历访问被拒绝</string>
|
||||
<string name="permission_denied_body">Calendula 无法在没有日历访问权限的情况下显示事件。您可以在系统设置中重新授予权限。</string>
|
||||
<string name="permission_open_settings_button">打开系统设置</string>
|
||||
<string name="permission_retry_button">重试</string>
|
||||
<string name="permission_benefit_private_title">保留在设备</string>
|
||||
<string name="permission_benefit_private_body">您的日历数据将会在本地读取,而不会被上传。</string>
|
||||
<string name="permission_benefit_sync_title">所有日历</string>
|
||||
<string name="permission_benefit_sync_body">Google、CalDAV、本地设备——任何同步到设备的东西都会直接显示出来。</string>
|
||||
<string name="permission_benefit_privacy_title">无追踪器</string>
|
||||
<string name="permission_benefit_privacy_body">零遥测,零分析,没有广告。</string>
|
||||
<string name="permission_privacy_footnote">数据仅保留在本地设备 ·无网络权限</string>
|
||||
<string name="month_prev">上个月</string>
|
||||
<string name="month_next">下个月</string>
|
||||
<string name="permission_benefit_private_title">从设计上保护隐私</string>
|
||||
<string name="permission_benefit_private_body">没有网络权限,没有遥测——任何数据都不会离开您的手机。</string>
|
||||
<string name="permission_benefit_sync_title">所有日历汇聚一处</string>
|
||||
<string name="permission_benefit_sync_body">Google、CalDAV、本地日历——只要同步到设备就会自动显示。</string>
|
||||
<string name="permission_benefit_privacy_title">永无追踪</string>
|
||||
<string name="permission_benefit_privacy_body">零遥测、零分析、无广告。</string>
|
||||
<string name="permission_privacy_footnote">数据保留在您的设备 · 无网络权限</string>
|
||||
<string name="month_prev">上月</string>
|
||||
<string name="month_next">下月</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_action_settings">设置</string>
|
||||
<string name="month_a11y_today_prefix">今天</string>
|
||||
@@ -43,80 +42,572 @@
|
||||
<string name="event_detail_edit">编辑</string>
|
||||
<string name="event_detail_delete">删除</string>
|
||||
<string name="event_detail_share">分享</string>
|
||||
<string name="event_share_chooser_title">分享活动</string>
|
||||
<string name="event_share_failed">无法分享该活动。</string>
|
||||
<string name="event_delete_title">删除活动?</string>
|
||||
<string name="event_delete_body">该活动将从你的日历以及所有同步设备中移除。</string>
|
||||
<string name="event_delete_recurring_title">删除重复活动</string>
|
||||
<string name="event_delete_option_occurrence">仅该活动</string>
|
||||
<string name="event_delete_option_following">当前及接下来的所有活动</string>
|
||||
<string name="event_delete_option_series">系列中的所有活动</string>
|
||||
<string name="event_edit_recurring_title">编辑重复活动</string>
|
||||
<string name="event_delete_failed">无法删除该活动</string>
|
||||
<string name="event_delete_write_denied">Calendula需要权限读取或删除活动</string>
|
||||
<string name="event_share_chooser_title">分享事件</string>
|
||||
<string name="event_share_failed">无法分享此事件。</string>
|
||||
<string name="event_delete_title">删除事件?</string>
|
||||
<string name="event_delete_body">该事件将从您的日历以及所有同步设备中移除。</string>
|
||||
<string name="event_delete_recurring_title">删除重复事件</string>
|
||||
<string name="event_delete_option_occurrence">仅此事件</string>
|
||||
<string name="event_delete_option_following">此事件及其后的所有事件</string>
|
||||
<string name="event_delete_option_series">整个系列的所有事件</string>
|
||||
<string name="event_edit_recurring_title">编辑重复事件</string>
|
||||
<string name="event_delete_failed">无法删除事件</string>
|
||||
<string name="event_delete_write_denied">Calendula 需要写入权限才能删除事件</string>
|
||||
<string name="dialog_cancel">取消</string>
|
||||
<string name="dialog_ok">确认</string>
|
||||
<string name="event_edit_new_title">新活动</string>
|
||||
<string name="dialog_ok">确定</string>
|
||||
<string name="event_edit_new_title">新事件</string>
|
||||
<string name="event_edit_close">关闭</string>
|
||||
<string name="event_edit_save">保存</string>
|
||||
<string name="event_edit_title_hint">增加标题</string>
|
||||
<string name="event_edit_managed_hint">由“%1$s”管理——标题、日期和重复内容会与你的联系人保持同步。提醒、位置和笔记由你自行编辑。</string>
|
||||
<string name="event_edit_title_hint">添加标题</string>
|
||||
<string name="event_edit_managed_hint">由“%1$s”管理——标题、日期和重复会从您的联系人同步。提醒、位置和备注由您编辑。</string>
|
||||
<string name="event_edit_starts">开始</string>
|
||||
<string name="event_edit_ends">结束</string>
|
||||
<string name="event_edit_error_end_before_start">报错:结束时间早于起始时间</string>
|
||||
<string name="event_edit_error_no_calendar">无可写入的日历</string>
|
||||
<string name="event_edit_save_failed">无法保存该活动</string>
|
||||
<string name="event_edit_write_denied">Calendula需要写入权限来创建活动</string>
|
||||
<string name="event_edit_more_fields">其他领域</string>
|
||||
<string name="event_edit_add">新增</string>
|
||||
<string name="event_edit_add_reminder">新建提醒</string>
|
||||
<string name="event_edit_error_end_before_start">结束时间早于开始时间</string>
|
||||
<string name="event_edit_error_no_calendar">没有可写入的日历</string>
|
||||
<string name="event_edit_save_failed">无法保存事件</string>
|
||||
<string name="event_edit_write_denied">Calendula 需要写入权限才能创建事件</string>
|
||||
<string name="event_edit_more_fields">更多字段</string>
|
||||
<string name="event_edit_add">添加</string>
|
||||
<string name="event_edit_add_reminder">添加提醒</string>
|
||||
<string name="event_edit_remove_reminder">移除提醒</string>
|
||||
<string name="event_edit_attendees">访问者</string>
|
||||
<string name="event_edit_add_guest">新增访问者</string>
|
||||
<string name="event_edit_add_guest_hint">通过 Email 新增一个访问者…</string>
|
||||
<string name="event_edit_add_guest_from_contacts">从联系人中添加访问者</string>
|
||||
<string name="event_edit_location_from_contacts">从联系人中添加地址</string>
|
||||
<string name="event_edit_remove_guest">移除访问者</string>
|
||||
<string name="event_edit_attendee_required">必填</string>
|
||||
<string name="event_edit_attendee_optional">可选项</string>
|
||||
<string name="event_edit_attendees_note_synced">Calendula 不发送邀请函。你的日历账户同步时可能会给访客发邮件。</string>
|
||||
<string name="event_edit_attendees_note_local">保存在本设备,其他访客不会被提醒。</string>
|
||||
<string name="event_edit_attendees">参与者</string>
|
||||
<string name="event_edit_add_guest">添加参与者</string>
|
||||
<string name="event_edit_add_guest_hint">通过电子邮件添加参与者…</string>
|
||||
<string name="event_edit_add_guest_from_contacts">从联系人添加</string>
|
||||
<string name="event_edit_location_from_contacts">从联系人选取地址</string>
|
||||
<string name="event_edit_remove_guest">移除参与者</string>
|
||||
<string name="event_edit_attendee_required">必需</string>
|
||||
<string name="event_edit_attendee_optional">可选</string>
|
||||
<string name="event_edit_attendees_note_synced">Calendula 不发送邀请。您的日历帐户同步时可能会通过邮件通知参与者。</string>
|
||||
<string name="event_edit_attendees_note_local">存储于本设备,不会通知任何人。</string>
|
||||
<string name="event_edit_reminder_custom">自定义</string>
|
||||
<string name="reminder_unit_minutes">分钟</string>
|
||||
<string name="reminder_unit_hours">小时</string>
|
||||
<string name="reminder_unit_days">天</string>
|
||||
<string name="reminder_unit_weeks">周</string>
|
||||
<string name="event_edit_availability">空闲</string>
|
||||
<string name="event_edit_visibility">隐私设置</string>
|
||||
<string name="event_edit_color">色彩</string>
|
||||
<string name="event_edit_availability">忙闲状态</string>
|
||||
<string name="event_edit_visibility">可见性</string>
|
||||
<string name="event_edit_color">颜色</string>
|
||||
<string name="event_edit_color_default">日历颜色</string>
|
||||
<string name="event_edit_color_custom">自定义颜色</string>
|
||||
<string name="event_edit_color_reset">重置</string>
|
||||
<string name="event_edit_color_unsupported">该日历不可用</string>
|
||||
<string name="event_edit_color_unsupported_hint">此日历未设置任何颜色集。您可以在“设置”中为此类日历启用自定义颜色。</string>
|
||||
<string name="event_edit_color_sync_warning">该日历在下一次同步时可能会删除或覆盖该颜色。</string>
|
||||
<string name="event_edit_conflict_title">其他设备的活动已更改</string>
|
||||
<string name="event_edit_conflict_body">您在编辑时,该事件已被同步或其他应用修改。您的修改要怎么处理?</string>
|
||||
<string name="event_edit_color_unsupported">此日历不支持</string>
|
||||
<string name="event_edit_color_unsupported_hint">此日历未提供颜色集。您可以在“设置”中为此类日历允许自定义颜色。</string>
|
||||
<string name="event_edit_color_sync_warning">此日历下次同步时可能会丢弃或覆盖该颜色。</string>
|
||||
<string name="event_edit_conflict_title">事件已在别处更改</string>
|
||||
<string name="event_edit_conflict_body">您编辑时,此事件已通过同步或其他应用被修改。您的更改应如何处理?</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_hint">该活动保持现状</string>
|
||||
<string name="event_edit_gone_title">活动被删除</string>
|
||||
<string name="event_edit_gone_body">这个事件在您编辑期间已被删除了(比如在另一台设备上)。您的修改现在无法保存。</string>
|
||||
<string name="event_edit_recurrence_none">确认无需重复</string>
|
||||
<string name="event_edit_conflict_discard_hint">事件保持当前状态</string>
|
||||
<string name="event_edit_gone_title">事件已被删除</string>
|
||||
<string name="event_edit_gone_body">该事件在您编辑期间已被删除(例如在另一设备上)。您的更改无法再保存。</string>
|
||||
<string name="event_edit_recurrence_none">不重复</string>
|
||||
<string name="event_edit_recurrence_custom">自定义</string>
|
||||
<string name="event_edit_recurrence_every">每</string>
|
||||
<string name="recurrence_unit_days">天</string>
|
||||
<string name="recurrence_unit_weeks">周</string>
|
||||
<string name="recurrence_unit_months">月</string>
|
||||
<string name="recurrence_unit_years">年</string>
|
||||
<string name="event_edit_recurrence_ends">结束时间</string>
|
||||
<string name="event_edit_recurrence_end_never">从不</string>
|
||||
<string name="event_edit_recurrence_ends">结束</string>
|
||||
<string name="event_edit_recurrence_end_never">永不</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_error_recurrence_ends_before_start">报错:结束日期早于开始日期</string>
|
||||
<string name="event_availability_busy">占用中</string>
|
||||
<string name="event_edit_error_recurrence_ends_before_start">重复结束日期早于事件开始时间</string>
|
||||
<string name="event_availability_busy">忙碌</string>
|
||||
<string name="event_access_default">默认</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>
|
||||
|
||||
@@ -128,7 +128,8 @@ class EventWriteMapperTest {
|
||||
original: EventForm,
|
||||
updated: EventForm,
|
||||
series: Long = seriesStart,
|
||||
): Map<String, Any?> = buildEventUpdateValues(original, updated, series, berlin)
|
||||
exdate: String? = null,
|
||||
): Map<String, Any?> = buildEventUpdateValues(original, updated, series, exdate, berlin)
|
||||
|
||||
/** The instant [local] names in [zoneId], as the provider would store it. */
|
||||
private fun instantAt(local: String, zoneId: String): Long =
|
||||
@@ -558,172 +559,237 @@ class EventWriteMapperTest {
|
||||
assertThat(values[CalendarContract.Events.ALL_DAY]).isEqualTo(1)
|
||||
}
|
||||
|
||||
// --- toDetachedOccurrence ("edit only this event", no _sync_id) ---
|
||||
// --- EXDATE follows the series when its times change (Codeberg #248) ---
|
||||
|
||||
/** A weekly series whose displayed occurrence runs 15 July 2026, 09:00–10:00. */
|
||||
private fun julySeries(): EventForm = form(
|
||||
start = LocalDateTime(LocalDate(2026, 7, 15), LocalTime(9, 0)),
|
||||
end = LocalDateTime(LocalDate(2026, 7, 15), LocalTime(10, 0)),
|
||||
).copy(rrule = "FREQ=WEEKLY")
|
||||
|
||||
/** [julySeries] pushed to [hour]:00, the shift an "all events" time edit makes. */
|
||||
private fun EventForm.atHour(hour: Int): EventForm = copy(
|
||||
start = LocalDateTime(LocalDate(2026, 7, 15), LocalTime(hour, 0)),
|
||||
end = LocalDateTime(LocalDate(2026, 7, 15), LocalTime(hour + 1, 0)),
|
||||
)
|
||||
|
||||
@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")
|
||||
fun `a series time edit moves its exclusions with the anchor`() {
|
||||
// The bug: the stamp stayed at the old instant, which the moved series no
|
||||
// longer generates, so the occurrence the user deleted came back.
|
||||
val series = instantAt("2026-07-01T09:00", "Europe/Berlin")
|
||||
val original = julySeries()
|
||||
// 8 July 09:00 Berlin (CEST, +2) == 07:00Z; at 10:00 it must read 08:00Z.
|
||||
val values = update(original, original.atHour(10), series, "20260708T070000Z")
|
||||
assertThat(values[CalendarContract.Events.EXDATE]).isEqualTo("20260708T080000Z")
|
||||
}
|
||||
|
||||
@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)
|
||||
fun `an exclusion keeps its wall clock across a DST boundary`() {
|
||||
// Anchor and edited occurrence are in July (CEST, +2); the excluded
|
||||
// occurrence sits in January (CET, +1). Shifting by the millisecond delta
|
||||
// measured at the edited occurrence would leave it an hour off.
|
||||
val series = instantAt("2026-01-07T09:00", "Europe/Berlin")
|
||||
val original = julySeries()
|
||||
// 14 January 09:00 Berlin == 08:00Z; at 10:00 it must read 09:00Z.
|
||||
val values = update(original, original.atHour(10), series, "20260114T080000Z")
|
||||
assertThat(values[CalendarContract.Events.EXDATE]).isEqualTo("20260114T090000Z")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a detached all-day occurrence stays on UTC midnights`() {
|
||||
val edited = form(
|
||||
fun `pinning a series to another zone re-resolves its exclusions there`() {
|
||||
val series = instantAt("2026-07-01T09:00", "Europe/Berlin")
|
||||
val original = julySeries()
|
||||
val values = update(
|
||||
original,
|
||||
original.copy(timezone = "Asia/Tokyo"),
|
||||
series,
|
||||
"20260716T070000Z",
|
||||
)
|
||||
// The exclusion still reads 09:00 — now 09:00 in Tokyo (+9) == 00:00Z.
|
||||
assertThat(values[CalendarContract.Events.EXDATE]).isEqualTo("20260716T000000Z")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an all-day series move shifts its exclusions by whole days`() {
|
||||
val series = instantAt("2026-07-01T00:00", "UTC")
|
||||
val original = 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),
|
||||
start = LocalDateTime(LocalDate(2026, 7, 15), LocalTime(0, 0)),
|
||||
end = LocalDateTime(LocalDate(2026, 7, 15), LocalTime(0, 0)),
|
||||
).copy(rrule = "FREQ=WEEKLY")
|
||||
val moved = original.copy(
|
||||
start = LocalDateTime(LocalDate(2026, 7, 17), LocalTime(0, 0)),
|
||||
end = LocalDateTime(LocalDate(2026, 7, 17), LocalTime(0, 0)),
|
||||
)
|
||||
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)
|
||||
|
||||
assertThat(update(original, moved, series, "20260722")[CalendarContract.Events.EXDATE])
|
||||
.isEqualTo("20260724")
|
||||
}
|
||||
|
||||
@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")
|
||||
fun `switching a series to all-day rewrites its exclusions as dates`() {
|
||||
// The two forms aren't interchangeable: a date-time stamp on an all-day
|
||||
// series matches no occurrence, so the exclusion would be lost.
|
||||
val series = instantAt("2026-07-01T09:00", "Europe/Berlin")
|
||||
val original = julySeries()
|
||||
val values = update(original, original.copy(isAllDay = true), series, "20260722T070000Z")
|
||||
assertThat(values[CalendarContract.Events.EXDATE]).isEqualTo("20260722")
|
||||
}
|
||||
|
||||
@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(
|
||||
fun `switching a series back to timed rewrites its exclusions as instants`() {
|
||||
val series = instantAt("2026-07-01T00:00", "UTC")
|
||||
val original = 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),
|
||||
start = LocalDateTime(LocalDate(2026, 7, 15), LocalTime(0, 0)),
|
||||
end = LocalDateTime(LocalDate(2026, 7, 15), LocalTime(0, 0)),
|
||||
).copy(rrule = "FREQ=WEEKLY")
|
||||
val timed = original.copy(
|
||||
isAllDay = false,
|
||||
start = LocalDateTime(LocalDate(2026, 7, 15), LocalTime(9, 0)),
|
||||
end = LocalDateTime(LocalDate(2026, 7, 15), LocalTime(10, 0)),
|
||||
)
|
||||
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()
|
||||
// The excluded day gains the new 09:00 Berlin time-of-day == 07:00Z.
|
||||
assertThat(update(original, timed, series, "20260722")[CalendarContract.Events.EXDATE])
|
||||
.isEqualTo("20260722T070000Z")
|
||||
}
|
||||
|
||||
@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()
|
||||
fun `a text-only edit leaves the exclusions alone`() {
|
||||
val original = julySeries()
|
||||
val values = update(original, original.copy(title = "Renamed"), exdate = "20260722T070000Z")
|
||||
assertThat(values).doesNotContainKey(CalendarContract.Events.EXDATE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an exclusion is found anywhere in a multi-entry exdate list`() {
|
||||
// Whitespace after a comma is legal in the stored column.
|
||||
fun `changing only the rule rewrites no exclusions`() {
|
||||
// The times are untouched, so every surviving occurrence keeps its instant
|
||||
// and the stamps still name the right ones.
|
||||
val original = julySeries()
|
||||
val values = update(original, original.copy(rrule = "FREQ=DAILY"), exdate = "20260722T070000Z")
|
||||
assertThat(values[CalendarContract.Events.RRULE]).isEqualTo("FREQ=DAILY")
|
||||
assertThat(values).doesNotContainKey(CalendarContract.Events.EXDATE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an exdate form we do not write is left untouched`() {
|
||||
// A sync adapter may store a TZID-parameterised or floating stamp. A stale
|
||||
// stamp excludes nothing; a mangled one could exclude the wrong occurrence.
|
||||
val original = julySeries()
|
||||
val values = update(
|
||||
original,
|
||||
original.atHour(10),
|
||||
instantAt("2026-07-01T09:00", "Europe/Berlin"),
|
||||
"TZID=Europe/Berlin;20260722T090000",
|
||||
)
|
||||
assertThat(values).doesNotContainKey(CalendarContract.Events.EXDATE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removing the recurrence clears the exclusions with it`() {
|
||||
// Dormant, not harmless: adding a recurrence back later would punch the old
|
||||
// holes into the new one.
|
||||
val original = julySeries()
|
||||
val values = update(original, original.copy(rrule = null), exdate = "20260722T070000Z")
|
||||
assertThat(values).containsEntry(CalendarContract.Events.EXDATE, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `undoing a series move lands the exclusions back where they started`() {
|
||||
val series = instantAt("2026-01-07T09:00", "Europe/Berlin")
|
||||
val original = julySeries()
|
||||
val moved = original.copy(
|
||||
start = LocalDateTime(LocalDate(2026, 7, 17), LocalTime(14, 30)),
|
||||
end = LocalDateTime(LocalDate(2026, 7, 17), LocalTime(15, 30)),
|
||||
)
|
||||
val exdate = "20260722T070000Z"
|
||||
|
||||
val forward = update(original, moved, series, exdate)
|
||||
val movedExdate = forward[CalendarContract.Events.EXDATE] as String
|
||||
assertThat(movedExdate).isNotEqualTo(exdate)
|
||||
|
||||
val back = update(
|
||||
moved,
|
||||
original,
|
||||
forward[CalendarContract.Events.DTSTART] as Long,
|
||||
movedExdate,
|
||||
)
|
||||
assertThat(back[CalendarContract.Events.EXDATE]).isEqualTo(exdate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a series with no exclusions writes no exdate column`() {
|
||||
val series = instantAt("2026-07-01T09:00", "Europe/Berlin")
|
||||
val original = julySeries()
|
||||
assertThat(update(original, original.atHour(10), series, exdate = null))
|
||||
.doesNotContainKey(CalendarContract.Events.EXDATE)
|
||||
}
|
||||
|
||||
// --- exdateAfter (the exclusions a "this and following" split inherits) ---
|
||||
|
||||
@Test
|
||||
fun `a split carries only the exclusions past the split point`() {
|
||||
// The occurrence at the split point is the one being edited, so it exists
|
||||
// by definition — a stale exclusion for it would swallow the edit whole.
|
||||
assertThat(
|
||||
exdateContains(
|
||||
"20260604T080000Z, 20260611T080000Z,20260618T080000Z",
|
||||
1_781_164_800_000L,
|
||||
exdateAfter(
|
||||
existingExdate = "20260708T080000Z,20260715T080000Z,20260722T080000Z",
|
||||
beginMillis = instantAt("2026-07-15T08:00", "UTC"),
|
||||
isAllDay = false,
|
||||
),
|
||||
).isTrue()
|
||||
).isEqualTo("20260722T080000Z")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a split carries nothing when every exclusion is behind it`() {
|
||||
assertThat(
|
||||
exdateAfter("20260708T080000Z", instantAt("2026-07-15T08:00", "UTC"), isAllDay = false),
|
||||
).isNull()
|
||||
assertThat(exdateAfter(null, 0L, isAllDay = false)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all-day exclusions split on their UTC date`() {
|
||||
assertThat(
|
||||
exdateAfter("20260708,20260722", instantAt("2026-07-15T00:00", "UTC"), isAllDay = true),
|
||||
).isEqualTo("20260722")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unreadable exclusion carries nothing across a split`() {
|
||||
assertThat(
|
||||
exdateAfter(
|
||||
"20260722T080000Z,TZID=Europe/Berlin;20260729T100000",
|
||||
instantAt("2026-07-15T08:00", "UTC"),
|
||||
isAllDay = false,
|
||||
),
|
||||
).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `split exclusions move by the same shift as the new series start`() {
|
||||
// What the split path composes: filter to the tail, then re-time it by the
|
||||
// shift the user applied to the occurrence they split at.
|
||||
val original = julySeries()
|
||||
val carried = shiftedExdate(
|
||||
existingExdate = exdateAfter(
|
||||
"20260716T070000Z",
|
||||
instantAt("2026-07-15T07:00", "UTC"),
|
||||
isAllDay = false,
|
||||
),
|
||||
original = original,
|
||||
updated = original.atHour(11),
|
||||
zone = berlin,
|
||||
)
|
||||
// 16 July 09:00 Berlin, pushed two hours, is 11:00 Berlin == 09:00Z.
|
||||
assertThat(carried).isEqualTo("20260716T090000Z")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nothing to shift yields no exdate`() {
|
||||
assertThat(shiftedExdate(null, julySeries(), julySeries().atHour(10), berlin)).isNull()
|
||||
assertThat(shiftedExdate(" ", julySeries(), julySeries().atHour(10), berlin)).isNull()
|
||||
}
|
||||
|
||||
// --- per-event colour ---
|
||||
@@ -914,4 +980,168 @@ class EventWriteMapperTest {
|
||||
val values = buildCopiedExceptionValues(exceptionSnapshot(duration = "P900S", dtEndMillis = null))
|
||||
assertThat(values[CalendarContract.Events.DURATION]).isEqualTo("P900S")
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
@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 `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 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 `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()
|
||||
}
|
||||
|
||||
@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 `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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ lifecycleCompose = "2.10.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"
|
||||
work = "2.10.1"
|
||||
work = "2.11.2"
|
||||
documentfile = "1.1.0"
|
||||
|
||||
[libraries]
|
||||
|
||||
Reference in New Issue
Block a user