diff --git a/README.md b/README.md
index 2dfc930..14ef73a 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,9 @@
BusyMax is a Linux desktop calendar and task manager built with Flutter.
-It brings calendar events and tasks into a native-feeling Linux desktop interface, with support for `Google Calendar`, `Google Tasks`, `Microsoft Calendar`, and `Microsoft To Do`.
+It brings calendar events and tasks into a native-feeling Linux desktop
+interface. BusyMax connects directly to Google, Microsoft, Apple iCloud
+Calendar, and Nextcloud. Apple Reminders is not supported.
[](https://snapcraft.io/busymax)
@@ -20,10 +22,18 @@ It brings calendar events and tasks into a native-feeling Linux desktop interfac
- Linux desktop app built with Flutter.
- Calendar views for day, week, month, year, and agenda planning.
-- Task creation with lists, due dates, reminders, and repeat options.
-- Event editing with calendar selection, time controls, repeat rules, and reminders.
+- Task creation with lists, start/due dates, reminders, repeat options,
+ subtasks, status, progress, priority, categories, location, and URL.
+- Nextcloud task-list creation/rename/delete, recursive task duplicate/delete,
+ raw iCalendar export, clear-completed, and cross-list subtree moves.
+- Event editing with calendar selection, time controls, repeat rules, and
+ reminders.
- Tray shortcut for opening the main Agenda view.
-- Integrations with Google Calendar, Google Tasks, Microsoft Calendar, and Microsoft To Do.
+- Direct integrations with Google Calendar, Google Tasks, Microsoft Calendar,
+ Microsoft To Do, Apple iCloud Calendar, Nextcloud Calendar, and Nextcloud
+ Tasks.
+- Offline-first local cache, conditional DAV writes, recurrence exceptions,
+ alarms, and explicit conflict handling for CalDAV accounts.
## Screenshots
@@ -59,6 +69,12 @@ It brings calendar events and tasks into a native-feeling Linux desktop interfac
+
+
+
+
@@ -71,12 +87,20 @@ It brings calendar events and tasks into a native-feeling Linux desktop interfac
## Prerequisites
-- Flutter: https://docs.flutter.dev/install
+- [Flutter SDK](https://docs.flutter.dev/install)
- GTK 3 and libhandy development packages (`libgtk-3-dev` and
`libhandy-1-dev` on Ubuntu/Debian)
-- `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET`, see [Google Setup](docs/google_setup.md)
+- `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET`; see
+ [Google setup](docs/google_setup.md)
- `MICROSOFT_OAUTH_CLIENT_ID`, see [Microsoft Setup](docs/microsoft_setup.md)
+Apple and Nextcloud do not require compile-time client credentials:
+
+- [Apple iCloud Calendar setup](docs/apple_icloud_setup.md) requires two-factor
+ authentication and an Apple app-specific password.
+- [Nextcloud setup](docs/nextcloud_setup.md) requires an HTTPS server and
+ completes authorization in the default browser.
+
## Run locally
Register the development launcher once so GNOME can associate BusyMax's native
@@ -127,7 +151,15 @@ flutter run -d linux \
The local Snap helper accepts the same value with
`--dart-define BUSYSTACK_FEEDBACK_ENDPOINT=http://127.0.0.1:8090/api/feedback`.
-No API, CAPTCHA, or other private server credential is used by the desktop application.
+No API, CAPTCHA, or other private server credential is used by the desktop
+application.
+
+## Provider support
+
+The [provider capability matrix](docs/provider_support_matrix.md) lists the
+features and limitations of each integration. Maintainers can use the
+[live-provider test guide](docs/live_provider_testing.md) for opt-in Nextcloud
+and iCloud integration tests.
## Build and publish the Snap
diff --git a/docs/apple_icloud_setup.md b/docs/apple_icloud_setup.md
new file mode 100644
index 0000000..11daab7
--- /dev/null
+++ b/docs/apple_icloud_setup.md
@@ -0,0 +1,67 @@
+# Apple iCloud Calendar setup
+
+BusyMax connects directly to Apple iCloud Calendar over CalDAV. This profile
+synchronizes calendar collections and `VEVENT` resources only. It does not
+connect to Apple Reminders.
+
+## Before connecting
+
+You need:
+
+- an Apple Account with two-factor authentication enabled;
+- the email address used by that Apple Account; and
+- a dedicated app-specific password for BusyMax.
+
+Do not enter your primary Apple Account password in BusyMax. Apple documents
+app-specific passwords as the fallback for third-party apps that cannot use
+Apple's account-authorization contract. BusyMax's Linux client does not use an
+undocumented Apple authorization flow.
+
+## Create the password and connect
+
+1. Sign in at [account.apple.com](https://account.apple.com/).
+2. Open **Sign-In and Security**, then **App-Specific Passwords**.
+3. Generate a password with a recognizable label such as `BusyMax Linux`.
+4. In BusyMax, open **Add account** and choose **Apple iCloud Calendar**.
+5. Enter the Apple Account email and the complete generated password. BusyMax
+ trims accidental whitespace but otherwise treats the password as opaque.
+6. Select **Connect**. BusyMax validates the credential and discovers the
+ account's calendars before saving it.
+
+BusyMax starts discovery at `https://caldav.icloud.com/`, follows only
+validated Apple iCloud CalDAV destinations, and requires normal platform TLS
+certificate validation. There is no invalid-certificate, HTTP, or custom
+iCloud-server option.
+
+## Calendars and editing
+
+BusyMax shows discovered event calendars and derives whether each one is
+writable from DAV privileges. Shared or subscribed read-only calendars remain
+visible but their edit controls are disabled. Event content is cached locally
+for offline viewing; offline edits to writable calendars are queued and later
+sent with conditional ETag checks.
+
+Calendar collection creation, deletion, rename, color, and ordering are not
+supported for iCloud. Invitations and scheduling changes are also not
+supported.
+
+## Reconnect, revoke, or remove
+
+- If Apple rejects the credential, existing cached data and pending work stay
+ local. Generate a replacement app-specific password, then use **Reconnect**.
+- To revoke access remotely, remove the BusyMax password at
+ [account.apple.com](https://account.apple.com/) under **App-Specific
+ Passwords**. BusyMax cannot revoke an Apple password through CalDAV.
+- Removing the account from BusyMax removes its local credential, cached DAV
+ objects, projections, cursors, conflicts, and pending operations. It does
+ not revoke the remote Apple password; revoke it manually as well.
+- Apple states that changing or resetting the primary Apple Account password
+ automatically revokes all app-specific passwords.
+
+Apple's current instructions are [Sign in to apps with your Apple Account
+using app-specific passwords](https://support.apple.com/en-gb/102654) and
+[Access your iCloud Mail, Calendar and Contacts in third-party
+apps](https://support.apple.com/en-ie/121539).
+
+This setup is specific to Apple iCloud and cannot be used for arbitrary CalDAV
+servers.
diff --git a/docs/beta_snap_release.md b/docs/beta_snap_release.md
index 84c0fa8..da9e748 100644
--- a/docs/beta_snap_release.md
+++ b/docs/beta_snap_release.md
@@ -43,6 +43,12 @@ See [Google Setup](google_setup.md) and
and can be extracted, so use only native Desktop/public-client credentials.
Never use server credentials or commit the JSON or generated `.snap` files.
+Apple iCloud Calendar and Nextcloud do not use compile-time client secrets.
+Read [Apple iCloud setup](apple_icloud_setup.md) and [Nextcloud
+setup](nextcloud_setup.md). Apple requires a per-user app-specific password;
+Nextcloud creates a per-client app password through Login Flow v2 in the
+default browser. Never put either credential in the defines file.
+
## Build
From the repository root:
@@ -117,11 +123,32 @@ Before upload, verify:
- Desktop search shows one BusyMax launcher; the tray Agenda action opens the
Agenda view in the main window.
- Google and Microsoft sign-in complete successfully.
+- Apple iCloud setup accepts only an Apple Account email and app-specific
+ password, discovers calendars over verified TLS, and reconnects after a
+ controlled app-password revocation.
+- Nextcloud Login Flow v2 opens the system default browser, completes after the
+ user returns to BusyMax, preserves an installation path, and uses the
+ server-returned canonical credentials.
- Tasks and events can be created, edited, completed, and deleted; a task
created in Agenda appears immediately without manual refresh.
- Accounts, settings, and data survive restart.
+- The XDG Secret portal-backed encrypted credential file works while strictly
+ confined: connect, quit, restart the desktop session if practical, reopen,
+ sync, reconnect, then remove the account and confirm its local credential is
+ gone.
+- Revoked Apple/Nextcloud credentials pause synchronization while cached data
+ and pending work remain visible.
+- Read-only/shared DAV collections remain visible but do not expose mutation
+ controls; a server ACL change is enforced after refresh.
+- Network, DNS, platform TLS rejection, recurrence/alarm projection, and
+ notifications work under confinement.
- Notifications and tray actions, including opening Agenda in the main window
and Quit, work.
+- Upgrade a copy of data from the last released package and verify schema-5 to
+ schema-8 migration, existing provider credentials/cursors/pending
+ operations, DAV projections, and account removal/local cleanup.
+- `snap/snapcraft.yaml`, metainfo, and screenshots describe exactly the tested
+ providers. Apple wording says iCloud Calendar, not Apple Reminders.
## Upload To Beta
@@ -178,6 +205,10 @@ snap info busymax
Repeat the local smoke checks against the Store-delivered revision.
+Record the downloaded revision, channel, checksum, test machine, and smoke-test
+result in the release record. Do not include account identities, credentials,
+DAV resource paths, or calendar and task content.
+
Official references: [build environments](https://documentation.ubuntu.com/snapcraft/stable/reference/build-environment-options/),
[upload](https://documentation.ubuntu.com/snapcraft/stable/reference/commands/upload/),
and [revision management](https://documentation.ubuntu.com/snapcraft/stable/how-to/publishing/manage-revisions-and-releases/).
diff --git a/docs/google_setup.md b/docs/google_setup.md
index b8b5ccf..0333a34 100644
--- a/docs/google_setup.md
+++ b/docs/google_setup.md
@@ -1,78 +1,43 @@
-# Google Setup
+# Google OAuth setup
-This setup is required to get `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET`
+BusyMax requires a Google desktop OAuth client. Its client ID and client secret
+are supplied as `GOOGLE_OAUTH_CLIENT_ID` and
+`GOOGLE_OAUTH_CLIENT_SECRET` at build time.
-## 1 Enable APIs
+## Create a Google Cloud project
-### 1.1 Create New Project
+1. Open the [Google Cloud Console](https://console.cloud.google.com/).
+2. Create or select a project.
+3. Enable the Google Tasks API and Google Calendar API.
-Go to `Google Cloud Console`: https://console.cloud.google.com
+## Configure the consent screen
-Click `Open project picker` (top-left corner) and create a new project. Then, select it.
+1. Open [Google Auth Platform](https://console.cloud.google.com/auth/).
+2. Complete the initial setup with the application name, support email,
+ audience, and contact email.
+3. Under **Audience**, add development accounts as test users while the app is
+ in testing mode.
+4. Under **Data access**, add these scopes:
-### 1.1 Enable Task and Calendar API
+ ```text
+ openid
+ https://www.googleapis.com/auth/userinfo.email
+ https://www.googleapis.com/auth/userinfo.profile
+ https://www.googleapis.com/auth/tasks
+ https://www.googleapis.com/auth/calendar
+ ```
-Search and enable the following:
+The identity scopes provide the stable account identity and display label. The
+Tasks and Calendar scopes allow BusyMax to synchronize and edit the
+corresponding data.
-- Google Tasks API
-- Google Calendar API
+## Create the desktop client
-## 2 Google Auth Platform
+1. Open **Clients** and select **Create client**.
+2. Choose **Desktop app** as the application type.
+3. Give the client a recognizable name.
+4. Store the client ID and client secret securely and provide them to the build
+ as `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET`.
-### 2.1 Initial Setup
-
-Go to `Google Auth Platform`: https://console.cloud.google.com/auth/
-
-Click `Get Started`.
-
-Enter:
-
-- App name:
-- User support email:
-- Audience: `External`
-- Contact Information:
-
-Click `Save`.
-
-### 2.2 Branding
-
-Click `Branding` to provide additional information if needed.
-
-### 2.3 Audience
-
-Click `Audience` to add test users. While publishing status is set to "Testing", only test users are
-able to access the app.
-
-### 2.4 Clients
-
-Click `Clients` -> `Create Client` and enter:
-
-- Application type: Desktop app
-- Name:
-
-!!! **Important**: copy and save `Client ID` and `Client secret`. **You will no longer be able to
-view or download the client secret once you close this dialog. Make sure you have copied or
-downloaded the information below and securely stored it.** Use it as `GOOGLE_OAUTH_CLIENT_ID` and
-`GOOGLE_OAUTH_CLIENT_SECRET`.
-
-#### 2.5 Data Access
-
-Click `Add or remove scopes`
-
-Check the following:
-
-- openid
-- https://www.googleapis.com/auth/userinfo.email
-- https://www.googleapis.com/auth/userinfo.profile
-- https://www.googleapis.com/auth/tasks
-- https://www.googleapis.com/auth/calendar
-
-Click `Update` and `Save`.
-
-Rationale:
-
-```text
-openid/email/profile -> stable account identity and display label
-tasks -> Google Tasks create/edit/delete/sync
-calendar -> CalendarList, Calendars, Events, Colors, Freebusy support
-```
+Use only credentials created for this desktop application. Do not commit them
+to the repository.
diff --git a/docs/icalendar_data_model.md b/docs/icalendar_data_model.md
new file mode 100644
index 0000000..402c650
--- /dev/null
+++ b/docs/icalendar_data_model.md
@@ -0,0 +1,105 @@
+# iCalendar data model
+
+BusyMax keeps the server's complete iCalendar resource as the synchronization
+authority. It does not reconstruct DAV resources from normalized event or task
+rows.
+
+Two layers operate on each resource:
+
+- The document layer retains property order, unknown and duplicate properties,
+ parameters, sibling components, alarms, recurrence exceptions, and
+ `VTIMEZONE` data. Edits patch only the affected fields.
+- The semantic layer projects supported `VEVENT` and `VTODO` fields into the
+ local database for display, search, reminders, and recurrence expansion.
+
+Mutated resources use CRLF line endings and fold content lines at 75 UTF-8
+octets. Untouched data remains intact, including provider-specific extensions
+that BusyMax does not interpret.
+
+## Editable Nextcloud `VTODO` data
+
+The Nextcloud task editor is based on the official
+[Nextcloud Tasks 0.18.1 source](https://github.com/nextcloud/tasks/tree/v0.18.1).
+BusyMax can project and patch these properties without regenerating the rest of
+the resource:
+
+| Data | iCalendar representation |
+|---|---|
+| Identity and text | `UID`, `SUMMARY`, `DESCRIPTION`, `CATEGORIES` |
+| Scheduling | `DTSTART`, `DUE`, date-only, floating, UTC, or `TZID` values |
+| Progress | `STATUS`, `PERCENT-COMPLETE`, `COMPLETED` |
+| Details | `PRIORITY`, `LOCATION`, `URL`, `CLASS` |
+| Hierarchy and order | parent `RELATED-TO`, `X-APPLE-SORT-ORDER` |
+| Recurrence | `RRULE`, `RDATE`, `EXDATE`, `RECURRENCE-ID` |
+| Reminders | every `VALARM` child component |
+| Nextcloud UI state | `X-PINNED`, `X-OC-HIDESUBTASKS`, `X-OC-HIDECOMPLETEDSUBTASKS` |
+| Bookkeeping | `CREATED`, `LAST-MODIFIED`, `DTSTAMP` |
+
+Status, percentage, and completion date are changed together using the same
+state transitions as Nextcloud Tasks. In particular, reopening a 100-percent
+complete task changes it to 99 percent when an in-progress value is needed.
+Completing a parent completes its open descendants; reopening a descendant
+reopens a closed ancestor. Parent deletion is queued child-first.
+
+The recurrence editor covers the subset exposed by Nextcloud Tasks 0.18.1:
+`DAILY`, `WEEKLY`, `MONTHLY`, and `YEARLY`; `INTERVAL`; the supported `BYDAY`,
+`BYMONTH`, `BYMONTHDAY`, and `BYSETPOS` combinations; and either `COUNT` or
+`UNTIL`. Rules outside that subset, multiple `RRULE` properties, and detached
+instances remain synchronized and preserved but are not rewritten by the
+editor. Completing a recurring master creates the completed exception and
+advances the master when the rule has another occurrence.
+
+Every alarm remains in source order. The editor adds absolute reminders and
+before-start or before-due relative reminders. It applies the same editability
+rules as Nextcloud Tasks 0.18.1: absolute UTC triggers and supported
+start-relative triggers can be changed; due-relative, timed positive, and
+other unsupported trigger forms remain visible, removable, and intact. Alarm
+actions and additional properties are preserved. Removing a start or due value
+with related alarms requires the user to either remove those alarms or convert
+them to absolute triggers.
+
+## DAV task operations
+
+Task-object creates, updates, moves, and deletes are represented as durable
+pending operations. Updates use exact ETag preconditions. A cross-list move
+uses WebDAV `MOVE` with the original member name, `Depth: infinity`, and
+`Overwrite: F`; descendants are moved child-first so parent relationships stay
+valid. Recursive duplication assigns a new UID to the complete recurrence set
+and then duplicates descendants under their corresponding new parents. Clear
+completed selects closed root tasks and recursively deletes their descendants,
+including cancelled root trees as Nextcloud Tasks does.
+
+Creating a Nextcloud task list uses extended `MKCOL` with a `VTODO`-only
+supported component set, the Nextcloud default blue `#0082C9`, and the enabled
+property. Rename uses `PROPPATCH`; delete and unshare use `DELETE`. The list
+slug rules and collision suffixes follow Nextcloud Tasks 0.18.1.
+
+## Dependency decision
+
+No general-purpose iCalendar or CalDAV package is authoritative for this data.
+The evaluated packages normalized or regenerated resources in ways that could
+discard unsupported content or could not satisfy BusyMax's sync-token,
+conditional-write, redirect, and parser-limit requirements.
+
+The `xml` package is used only for namespace-aware WebDAV XML parsing. BusyMax
+adds response-size, depth, element-count, and text-size limits and rejects DTD
+and entity declarations before parsing.
+
+## Protocol references
+
+- [RFC 4791](https://www.rfc-editor.org/rfc/rfc4791): CalDAV collections,
+ reports, object resources, and write preconditions
+- [RFC 5545](https://www.rfc-editor.org/rfc/rfc5545): iCalendar syntax and
+ semantics
+- [RFC 4918](https://www.rfc-editor.org/rfc/rfc4918): WebDAV properties,
+ conditional writes, `PROPPATCH`, `MOVE`, and `DELETE`
+- [RFC 5689](https://www.rfc-editor.org/rfc/rfc5689): extended `MKCOL`
+- [RFC 6578](https://www.rfc-editor.org/rfc/rfc6578): WebDAV collection
+ synchronization
+- [RFC 6764](https://www.rfc-editor.org/rfc/rfc6764): service discovery
+- [RFC 7809](https://www.rfc-editor.org/rfc/rfc7809): time zones by reference
+
+The DAV and iCalendar test suites cover lossless field patches, task-state and
+hierarchy transitions, recurring completion, alarms, custom time zones,
+recursive duplicate/delete, cross-list `MOVE`, collection mutations, offline
+mutation replay, and server conflict handling.
diff --git a/docs/live_provider_testing.md b/docs/live_provider_testing.md
new file mode 100644
index 0000000..5a0e287
--- /dev/null
+++ b/docs/live_provider_testing.md
@@ -0,0 +1,121 @@
+# Live provider tests
+
+Live integration tests are opt-in and are skipped by a normal `flutter test`
+run. They create and delete remote calendars and objects, change sharing
+permissions, and may revoke app passwords. Run them only against disposable QA
+accounts and isolated test servers.
+
+Pass credentials through the test process environment. Do not commit them or
+include them in logs, screenshots, or bug reports.
+
+## Nextcloud DAV
+
+Set:
+
+```text
+BUSYMAX_NEXTCLOUD_LIVE=1
+BUSYMAX_NEXTCLOUD_LIVE_URL=
+BUSYMAX_NEXTCLOUD_LIVE_USERNAME=
+BUSYMAX_NEXTCLOUD_LIVE_PASSWORD=
+```
+
+Run:
+
+```bash
+flutter test test/dav/nextcloud_live_integration_test.dart
+```
+
+The URL may use HTTP only for an isolated loopback fixture. Production
+Nextcloud profiles require HTTPS.
+
+Set `BUSYMAX_NEXTCLOUD_LIVE_LARGE=1` to include the 128-member collection test.
+The restart test uses two separate runs:
+
+```text
+BUSYMAX_NEXTCLOUD_LIVE_RESTART_ID=
+BUSYMAX_NEXTCLOUD_LIVE_RESTART_STAGE=prepare
+```
+
+Restart the server without replacing its persistent storage, then rerun with
+`BUSYMAX_NEXTCLOUD_LIVE_RESTART_STAGE=verify`.
+
+## Nextcloud Login Flow v2
+
+Set:
+
+```text
+BUSYMAX_NEXTCLOUD_LOGIN_LIVE=1
+BUSYMAX_NEXTCLOUD_LOGIN_LIVE_URL=
+BUSYMAX_NEXTCLOUD_LOGIN_LIVE_USERNAME=
+BUSYMAX_NEXTCLOUD_LOGIN_LIVE_APP_PASSWORD=
+BUSYMAX_NEXTCLOUD_LOGIN_LIVE_TLS_CERT=
+BUSYMAX_NEXTCLOUD_LOGIN_LIVE_BROWSER=
+```
+
+Run:
+
+```bash
+flutter test test/dav/nextcloud_login_flow_live_test.dart
+```
+
+Run once at the server root and once through a path-prefixed installation such
+as `/nextcloud`. The test revokes the app password returned by Login Flow, so
+use credentials created for this purpose.
+
+## Nextcloud sharing
+
+Set:
+
+```text
+BUSYMAX_NEXTCLOUD_SHARING_LIVE=1
+BUSYMAX_NEXTCLOUD_SHARING_LIVE_URL=
+BUSYMAX_NEXTCLOUD_SHARING_LIVE_GROUP=
+BUSYMAX_NEXTCLOUD_SHARING_LIVE_OWNER_USERNAME=
+BUSYMAX_NEXTCLOUD_SHARING_LIVE_OWNER_PASSWORD=
+BUSYMAX_NEXTCLOUD_SHARING_LIVE_WRITER_USERNAME=
+BUSYMAX_NEXTCLOUD_SHARING_LIVE_WRITER_PASSWORD=
+BUSYMAX_NEXTCLOUD_SHARING_LIVE_READER_USERNAME=
+BUSYMAX_NEXTCLOUD_SHARING_LIVE_READER_PASSWORD=
+BUSYMAX_NEXTCLOUD_SHARING_LIVE_GROUP_MEMBER_USERNAME=
+BUSYMAX_NEXTCLOUD_SHARING_LIVE_GROUP_MEMBER_PASSWORD=
+```
+
+Run:
+
+```bash
+flutter test test/dav/nextcloud_sharing_live_test.dart
+```
+
+This test creates a calendar, changes user and group shares, verifies effective
+permissions, and removes the collection.
+
+## Apple iCloud Calendar
+
+Use a dedicated Apple QA account with two-factor authentication, a BusyMax-only
+app-specific password, and at least two calendars. Set:
+
+```text
+BUSYMAX_ICLOUD_LIVE=1
+BUSYMAX_ICLOUD_LIVE_USERNAME=
+BUSYMAX_ICLOUD_LIVE_PASSWORD=
+BUSYMAX_ICLOUD_LIVE_EXPECT_SHARED_WRITABLE=1
+BUSYMAX_ICLOUD_LIVE_EXPECT_SHARED_READ_ONLY=1
+```
+
+The two shared-calendar flags are optional and should be set only when those
+fixtures exist. Run:
+
+```bash
+flutter test test/dav/apple_icloud_live_integration_test.dart
+```
+
+The test covers discovery, collection permissions, conditional event writes,
+date and recurrence forms, alarms, conflict behavior, credential replacement,
+and local account removal. It does not replace a manual check in Apple Calendar
+or iCloud.com.
+
+## Recording results
+
+Record the source revision, provider/server versions, operating environment,
+test command, and result. Redact credentials, DAV resource paths, Login Flow
+URLs and tokens, raw iCalendar, and user content before sharing any output.
diff --git a/docs/microsoft_setup.md b/docs/microsoft_setup.md
index 2921733..49268da 100644
--- a/docs/microsoft_setup.md
+++ b/docs/microsoft_setup.md
@@ -1,38 +1,33 @@
-# Microsoft Setup
+# Microsoft OAuth setup
-This setup is required to get `MICROSOFT_OAUTH_CLIENT_ID`.
+BusyMax requires a Microsoft public-client application ID, supplied as
+`MICROSOFT_OAUTH_CLIENT_ID` at build time.
-## 1 Create app registration
+## Create the application registration
-Open Microsoft Entra: https://entra.microsoft.com
+1. Open [Microsoft Entra](https://entra.microsoft.com/).
+2. Go to **App registrations** and select **New registration**.
+3. Enter an application name.
+4. Select the account type that supports both organizational and personal
+ Microsoft accounts.
+5. Add a **Public client/native mobile and desktop** redirect URI:
-Go to:
+ ```text
+ http://localhost
+ ```
-```text
-App registrations -> New registration
-```
-
-Enter:
-- **Name**:
-- **Supported account types**: `Any Entra ID Tenant + Personal Microsoft accounts`
-- **Redirect**: `Public client/native mobile & desktop` with url `http://localhost`
-
-Click "Register".
-
-Copy `Application (client) ID` and use as `MICROSOFT_OAUTH_CLIENT_ID`.
+6. Register the application and copy its **Application (client) ID**. Supply
+ that value as `MICROSOFT_OAUTH_CLIENT_ID`.
-## 2 Add Microsoft Graph delegated permissions
+## Add delegated Microsoft Graph permissions
-Go to:
-
-```text
-App registrations -> -> API permissions -> Add a permission -> Microsoft Graph -> Delegated permissions
-```
-
-Add:
+Under **API permissions**, add these delegated Microsoft Graph permissions:
```text
User.Read
Tasks.ReadWrite
Calendars.ReadWrite
```
+
+Do not create or embed a client secret. BusyMax uses the public-client OAuth
+flow.
diff --git a/docs/nextcloud_setup.md b/docs/nextcloud_setup.md
new file mode 100644
index 0000000..91199ff
--- /dev/null
+++ b/docs/nextcloud_setup.md
@@ -0,0 +1,102 @@
+# Nextcloud Calendar and Tasks setup
+
+BusyMax connects directly to a selected Nextcloud server over CalDAV. It
+synchronizes `VEVENT` calendars and `VTODO` task lists, including mixed
+collections when the server exposes both component types.
+
+## Server requirements
+
+- Enter either the public HTTPS URL used in your browser or Nextcloud's
+ standard **primary CalDAV address**. For example, both
+ `https://cloud.example.net/nextcloud/` and
+ `https://cloud.example.net/nextcloud/remote.php/dav` are accepted.
+- BusyMax recognizes `/remote.php/dav` (including a copied calendar-specific
+ DAV path), removes the DAV suffix, and starts Login Flow v2 at the actual
+ Nextcloud installation root. You do not need to edit a primary CalDAV
+ address before pasting it.
+- The URL must use HTTPS, contain no username/password, and pass normal
+ platform certificate validation.
+- This release does not support HTTP-only servers, private-CA exceptions, an
+ invalid-certificate toggle, or arbitrary non-Nextcloud CalDAV accounts. A
+ standard Nextcloud CalDAV address is supported as an input shortcut.
+
+Reverse proxies must preserve the installation path and return the canonical
+public server URL. BusyMax accepts only same-origin Login Flow redirects that
+remain within that path contract.
+
+## Connect with the default browser
+
+1. In BusyMax, open **Add account** and choose **Nextcloud**.
+2. Enter the server URL or paste **Copy primary CalDAV address**, then select
+ **Continue in browser**.
+3. BusyMax anonymously starts Nextcloud Login Flow v2 and opens the login URL
+ in your system's default browser.
+4. Sign in there, complete any two-factor or identity-provider step, and grant
+ BusyMax access.
+5. Return to BusyMax. It polls the one-time endpoint until Nextcloud returns
+ the canonical server, `loginName`, and a dedicated app password.
+
+BusyMax never asks for or stores the user's primary Nextcloud password. The
+polling token and browser URL are treated as secrets and excluded from logs.
+Nextcloud documents that the polling token is valid for 20 minutes and a
+successful credential result is returned only once. Canceling the BusyMax
+dialog cancels local polling.
+
+## Calendars, task lists, and capabilities
+
+BusyMax inventories the account's DAV collections and exposes event and task
+views only when the advertised component set permits them. It uses ACL
+privileges as the primary writability signal:
+
+- read-only sources remain visible and cannot be mutated; shared sources use
+ the privileges granted by their owner;
+- calendar and task fields are enabled only when the relevant collection and
+ component capabilities are present;
+- a task shared with the account remains editable only when the collection is
+ writable and its classification is `PUBLIC`; classification itself cannot be
+ changed by the recipient;
+- unsupported scheduling or collection operations stay disabled rather than
+ being guessed from the server brand.
+
+For writable Nextcloud task lists, BusyMax supports the same task data model as
+the official Nextcloud Tasks 0.18.1 editor: start and due values, all-day state,
+status, percentage complete, completion time, iCalendar priority, description,
+categories, location, URL, classification, multiple alarms, recurrence,
+subtasks, pinning, and subtask-visibility flags. It also supports recursive
+duplicate and delete, raw iCalendar export, clear-completed, task ordering,
+and moving a complete task subtree between writable Nextcloud lists.
+
+Task lists can be created, renamed, deleted, or unshared when the server grants
+the required collection privileges. These collection changes require an online
+server round trip. List color/order editing, new-share administration, and the
+Nextcloud trash bin are not exposed by this release.
+
+Calendar and task objects are cached locally for offline use. Object writes,
+including cross-list task moves, are queued and use exact ETags when
+connectivity returns. A server-side concurrent edit is merged only when the
+changed fields are provably disjoint; otherwise BusyMax creates an explicit
+conflict for the user. The complete iCalendar resource remains authoritative,
+so unsupported properties and recurrence forms survive edits unchanged.
+
+## Reconnect, revoke, or remove
+
+- Use **Reconnect** to repeat Login Flow v2 after an app password is revoked.
+ Cached data and pending work remain available while reauthentication is
+ required.
+- When a Nextcloud account is removed, BusyMax attempts the official
+ authenticated app-password deletion endpoint, then always removes its local
+ credential and account data. A network or server failure can prevent remote
+ revocation, so check **Personal settings > Security > Devices & sessions**
+ and revoke the BusyMax token manually when removal reports a warning.
+- Revoking the BusyMax token in Nextcloud pauses synchronization without
+ deleting local pending work.
+
+Official protocol guidance: [Nextcloud Login Flow
+v2](https://docs.nextcloud.com/server/stable/developer_manual/client_apis/LoginFlow/index.html)
+and [Nextcloud WebDAV
+basics](https://docs.nextcloud.com/server/stable/developer_manual/client_apis/WebDAV/basic.html).
+Task behavior is cross-checked against the official
+[Nextcloud Tasks 0.18.1 source](https://github.com/nextcloud/tasks/tree/v0.18.1).
+
+For maintainer instructions covering disposable-server and live-account tests,
+see [Live provider tests](live_provider_testing.md).
diff --git a/docs/provider_support_matrix.md b/docs/provider_support_matrix.md
new file mode 100644
index 0000000..96d1d2f
--- /dev/null
+++ b/docs/provider_support_matrix.md
@@ -0,0 +1,77 @@
+# Provider capabilities
+
+This document describes the capabilities exposed by the current source tree.
+Actual write access also depends on the permissions reported by each account
+and collection. Unknown capabilities fail closed; BusyMax does not infer write
+access from a provider name.
+
+| Provider | Calendars | Tasks | Authentication |
+|---|---|---|---|
+| Google | Google Calendar | Google Tasks | OAuth desktop client |
+| Microsoft | Microsoft Calendar | Microsoft To Do | OAuth public client |
+| Apple iCloud | iCloud Calendar | Not supported; Apple Reminders is not supported | Apple app-specific password over HTTPS |
+| Nextcloud | `VEVENT` collections | `VTODO` collections | Login Flow v2 in the default browser |
+
+## CalDAV synchronization
+
+| Capability | Apple iCloud Calendar | Nextcloud Calendar | Nextcloud Tasks |
+|---|---|---|---|
+| Collection discovery | Supported | Supported | Supported |
+| Initial and incremental synchronization | Supported | Supported | Supported |
+| Offline object create, edit, delete, and replay | Writable collections | Writable collections | Writable collections |
+| Conditional ETag writes and explicit conflict handling | Supported | Supported | Supported |
+| All-day, floating, UTC, and `TZID` date-time values | Supported | Supported | Supported |
+| Recurrence rules and exceptions | Supported | Supported | Supported; see the editable subset below |
+| `VALARM` preservation | Supported | Supported | Supported |
+| Read-only and shared collections | Supported | Supported | Supported |
+| Collection create, rename, delete, or unshare | Not supported | Not supported | Supported online when permitted |
+| Collection color or order editing | Not supported | Not supported | Not supported |
+| Cross-list task moves | Not applicable | Not applicable | Supported between writable Nextcloud task collections |
+| Clear completed tasks | Not applicable | Not applicable | Supported |
+| Invitation, scheduling, and attendee changes | Not supported | Not supported | Not applicable |
+| HTTP-only or private-CA servers | Not supported | Not supported | Not supported |
+
+## Nextcloud task data
+
+BusyMax exposes the task data used by the official Nextcloud Tasks 0.18.1
+editor:
+
+- title, description, categories, start, due date, and all-day state;
+- status, completion percentage, completion date and time, and iCalendar
+ priority from 0 through 9;
+- location, URL, and `PUBLIC`, `CONFIDENTIAL`, or `PRIVATE` classification;
+- parent-child relationships, recursive subtasks, and Nextcloud's numeric task
+ order, including its `CREATED`-based fallback;
+- multiple reminders, including absolute and before-start/before-due triggers;
+- daily, weekly, monthly, and yearly recurrence with interval, supported
+ day/month selectors, count, or end date;
+- pinning and the Nextcloud subtask-visibility flags;
+- recursive duplication, raw iCalendar export, recursive deletion, moving a
+ subtree between lists, and deleting Nextcloud closed root task trees.
+
+Completing a recurring task creates a completed recurrence instance and
+advances the master task when another occurrence exists. Recurrence rules that
+the editor cannot represent remain intact and read-only. BusyMax also preserves
+unknown properties, parameters, duplicate properties, unsupported alarm
+actions, sibling components, and time-zone definitions instead of rebuilding
+the resource from the visible fields.
+
+The parity reference is the official
+[Nextcloud Tasks 0.18.1 source](https://github.com/nextcloud/tasks/tree/v0.18.1).
+The wire format and DAV operations follow
+[RFC 5545](https://www.rfc-editor.org/rfc/rfc5545),
+[RFC 4791](https://www.rfc-editor.org/rfc/rfc4791),
+[RFC 4918](https://www.rfc-editor.org/rfc/rfc4918), and
+[RFC 5689](https://www.rfc-editor.org/rfc/rfc5689).
+
+## Deliberate boundaries
+
+- Generic CalDAV account setup
+- Apple Reminders
+- Nextcloud Deck or Notes
+- Nextcloud task-list color/order editing, share administration, or trash-bin
+ management
+- Editing an individual detached task occurrence directly; synchronized
+ exceptions are preserved, and recurring completion is handled on the master
+- EventKit or private Apple APIs
+- HTTP fallback or invalid-certificate exceptions
diff --git a/docs/screenshots/account_provider_selection.png b/docs/screenshots/account_provider_selection.png
new file mode 100644
index 0000000..40787d4
Binary files /dev/null and b/docs/screenshots/account_provider_selection.png differ
diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb
index 6a96363..c1ac539 100644
--- a/lib/l10n/app_ar.arb
+++ b/lib/l10n/app_ar.arb
@@ -1,14 +1,14 @@
{
"@@locale": "ar",
"appTitle": "BusyMax",
- "connectGoogleAccount": "اربط حسابات Google وMicrosoft لمزامنة التقويمات والمهام.",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "في شاشة أذونات Google، حدّد أذونات التقويم والمهام معًا.",
"googlePermissionsRequiredRetry": "أذونات تقويم Google وGoogle Tasks مطلوبة. حاول مرة أخرى وحدّد مربعي الاختيار.",
"finishSetup": "إنهاء الإعداد",
"continueSetup": "متابعة",
"onboardingSetupTitle": "إعداد BusyMax",
"onboardingAccountsStepTitle": "ربط الحسابات",
- "onboardingAccountsStepDescription": "أضف جميع حسابات Google وMicrosoft التي تريد استخدامها. يزامن BusyMax التقويمات والأحداث وقوائم المهام والمهام من كل حساب.",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "اختيار إعدادات النظام",
"onboardingPreferencesStepDescription": "اضبط سلوك التطبيق على سطح المكتب والتذكيرات ومستوى تفاصيل الإشعارات والمظهر قبل فتح جدولك.",
"signInWithGoogle": "تسجيل الدخول باستخدام Google",
@@ -40,7 +40,7 @@
"showInSchedule": "إظهار في الجدول",
"noCalendarsSynced": "لم تتم مزامنة أي تقويمات بعد.",
"allDay": "طوال اليوم",
- "moreItems": "+\u2068{count}\u2069 عناصر أخرى",
+ "moreItems": "+{count} عناصر أخرى",
"noEventsOrTasks": "لا توجد أحداث أو مهام",
"scheduleLoading": "جارٍ تحميل الجدول...",
"scheduleUnavailable": "الجدول غير متاح",
@@ -93,10 +93,10 @@
"formatItalicTooltip": "مائل",
"formatUnderlineShortLabel": "U",
"formatUnderlineTooltip": "تحته خط",
- "reminderMinutesBefore": "{minutes, plural, =0{عند البدء} =1{قبل دقيقة واحدة} =2{قبل دقيقتين} few{قبل \u2068{minutes}\u2069 دقائق} many{قبل \u2068{minutes}\u2069 دقيقة} other{قبل \u2068{minutes}\u2069 دقيقة}}",
+ "reminderMinutesBefore": "{minutes, plural, =0{عند البدء} =1{قبل دقيقة واحدة} =2{قبل دقيقتين} few{قبل {minutes} دقائق} many{قبل {minutes} دقيقة} other{قبل {minutes} دقيقة}}",
"reminderAtStart": "عند البدء",
- "reminderHoursBefore": "{hours, plural, =0{عند البدء} =1{قبل ساعة واحدة} =2{قبل ساعتين} few{قبل \u2068{hours}\u2069 ساعات} many{قبل \u2068{hours}\u2069 ساعة} other{قبل \u2068{hours}\u2069 ساعة}}",
- "reminderDaysBefore": "{days, plural, =0{في اليوم نفسه} =1{قبل يوم واحد} =2{قبل يومين} few{قبل \u2068{days}\u2069 أيام} many{قبل \u2068{days}\u2069 يومًا} other{قبل \u2068{days}\u2069 يوم}}",
+ "reminderHoursBefore": "{hours, plural, =0{عند البدء} =1{قبل ساعة واحدة} =2{قبل ساعتين} few{قبل {hours} ساعات} many{قبل {hours} ساعة} other{قبل {hours} ساعة}}",
+ "reminderDaysBefore": "{days, plural, =0{في اليوم نفسه} =1{قبل يوم واحد} =2{قبل يومين} few{قبل {days} أيام} many{قبل {days} يومًا} other{قبل {days} يوم}}",
"availabilityFree": "متاح",
"availabilityTentative": "مبدئي",
"availabilityOutOfOffice": "خارج المكتب",
@@ -109,7 +109,7 @@
"sensitivityPersonal": "شخصي",
"tasks": "المهام",
"allTasks": "كل المهام",
- "tasksInList": "المهام في \u2068{title}\u2069",
+ "tasksInList": "المهام في {title}",
"taskLists": "قوائم المهام",
"navigation": "التنقل",
"mainMenu": "القائمة الرئيسية",
@@ -164,7 +164,7 @@
"feedbackRateLimitedError": "أُرسلت ملاحظات كثيرة جدًا من هذه الشبكة. انتظر وحاول مرة أخرى.",
"feedbackRejectedError": "رفض الخادم الإرسال. راجع الحقول وحاول مرة أخرى.",
"feedbackServerError": "يتعذر على BusyStack قبول ملاحظاتك الآن. لم تُمسح ملاحظاتك؛ حاول مرة أخرى.",
- "feedbackSuccess": "تم إرسال الملاحظات. المرجع: \u2068{id}\u2069",
+ "feedbackSuccess": "تم إرسال الملاحظات. المرجع: {id}",
"toggleSidebar": "إظهار الشريط الجانبي أو إخفاؤه",
"showSidebar": "إظهار اللوحة الجانبية",
"hideSidebar": "إخفاء اللوحة الجانبية",
@@ -179,14 +179,14 @@
"removeAccount": "إزالة الحساب…",
"removingAccount": "جارٍ إزالة الحساب…",
"removeAccountDescription": "إيقاف المزامنة وإزالة بيانات هذا الحساب من هذا الجهاز.",
- "removeAccountTitle": "إزالة \u2068{account}\u2069 من BusyMax؟",
- "removeAccountConfirmation": "سيؤدي ذلك إلى حذف المهام والتقويمات والأحداث والتذكيرات والتغييرات غير المتصلة المعلّقة المخزّنة مؤقتًا من هذا الجهاز. ستُفقد التغييرات غير المتزامنة. لن يُحذف أي شيء من Google أو Microsoft.",
+ "removeAccountTitle": "إزالة {account} من BusyMax؟",
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "إلغاء وصول BusyMax إلى حساب Google هذا أيضًا",
"revokeGoogleAccessDescription": "ستحتاج إلى منح الوصول مرة أخرى قبل إعادة الاتصال.",
"removeAccountAction": "إزالة الحساب",
"removeAccountFailed": "تعذر إكمال إزالة الحساب. حاول مرة أخرى.",
"accountRemovedGoogleRevokeFailed": "تمت إزالة الحساب من هذا الجهاز، لكن تعذر على BusyMax إلغاء الوصول إلى Google. يمكنك إلغاء الوصول من حسابك على Google.",
- "newList": "قائمة جديدة",
+ "newTaskList": "قائمة مهام جديدة",
"signInToViewTaskLists": "سجّل الدخول لعرض قوائم المهام.",
"noTaskListsSynced": "لم تتم مزامنة أي قوائم مهام بعد.",
"listActions": "إجراءات القائمة",
@@ -196,7 +196,7 @@
"deleteList": "حذف القائمة",
"builtInMicrosoftList": "مدمجة",
"builtInMicrosoftListCannotRenameDelete": "لا يمكن إعادة تسمية قوائم Microsoft To Do المدمجة أو حذفها.",
- "deleteListConfirmation": "حذف «\u2068{title}\u2069» من Google Tasks؟",
+ "deleteListConfirmation": "حذف «{title}» من Google Tasks؟",
"deleteEvent": "حذف الحدث",
"title": "العنوان",
"create": "إنشاء",
@@ -206,9 +206,9 @@
"refreshAll": "تحديث الكل",
"listRefreshed": "تم تحديث القائمة.",
"allTasksRefreshed": "تم تحديث جميع الحسابات.",
- "exportedFile": "تم التصدير إلى \u2068{path}\u2069",
- "exportFailed": "فشل التصدير: \u2068{error}\u2069",
- "refreshFailed": "فشل التحديث: \u2068{error}\u2069",
+ "exportedFile": "تم التصدير إلى {path}",
+ "exportFailed": "فشل التصدير: {error}",
+ "refreshFailed": "فشل التحديث: {error}",
"selectOrCreateTaskList": "اختر قائمة مهام أو أنشئ واحدة للبدء.",
"signInToViewTasks": "سجّل الدخول لعرض المهام.",
"noTasks": "لا توجد مهام.",
@@ -221,8 +221,8 @@
"upcoming": "القادمة",
"noDate": "بلا تاريخ",
"completed": "مكتملة",
- "duePrefix": "مستحقة في \u2068{date}\u2069",
- "dateTimeDisplay": "\u2068{date}\u2069 · \u2068{time}\u2069",
+ "duePrefix": "مستحقة في {date}",
+ "dateTimeDisplay": "{date} · {time}",
"taskDetails": "تفاصيل المهمة",
"editTask": "تعديل المهمة",
"noTaskSelected": "لم يتم تحديد مهمة.",
@@ -273,10 +273,11 @@
"list": "القائمة",
"microsoftMoveUnsupported": "نقل المهام بين القوائم غير مدعوم لحسابات Microsoft To Do في هذا الإصدار.",
"createSubtask": "إنشاء مهمة فرعية",
+ "subtasks": "مهام فرعية",
"moveToTop": "نقل إلى الأعلى",
"deleteTask": "حذف المهمة",
"newSubtask": "مهمة فرعية جديدة",
- "deleteTaskConfirmation": "حذف «\u2068{title}\u2069» من Google Tasks؟",
+ "deleteTaskConfirmation": "حذف «{title}»؟",
"metadata": "البيانات الوصفية",
"id": "المعرّف",
"etag": "ETag",
@@ -296,7 +297,7 @@
"startMinimizedToTray": "البدء مصغّرًا في شريط النظام",
"requiresTrayIcon": "يتطلب أيقونة شريط النظام.",
"syncComplete": "اكتملت المزامنة.",
- "syncFailed": "فشلت المزامنة: \u2068{error}\u2069",
+ "syncFailed": "فشلت المزامنة: {error}",
"notifySyncFailures": "إشعارات عند فشل المزامنة",
"notifyConflicts": "إشعارات عند حدوث تعارضات",
"notifyDueToday": "إشعارات المهام المستحقة اليوم",
@@ -325,7 +326,7 @@
"diagnostics": "التشخيصات",
"apiInspectorDisabled": "إظهار فاحص API",
"googleTasksApi": "واجهة Google Tasks API",
- "discoveryRevision": "مراجعة Discovery: \u2068{revision}\u2069",
+ "discoveryRevision": "مراجعة Discovery: {revision}",
"implementedMethods": "الطرق المنفذة",
"supportsTasksScopes": "يدعم نطاقَي tasks وtasks.readonly",
"requiresTasksScope": "يتطلب نطاق tasks",
@@ -333,9 +334,9 @@
"signInToInspectPendingOperations": "سجّل الدخول لفحص العمليات المعلّقة.",
"noBlockedPendingOperations": "لا توجد عمليات معلّقة محظورة.",
"operationActions": "إجراءات العملية",
- "pendingOpListId": "القائمة=\u2068{id}\u2069",
- "pendingOpTaskId": "المهمة=\u2068{id}\u2069",
- "pendingOpAttempts": "المحاولات=\u2068{count}\u2069",
+ "pendingOpListId": "القائمة={id}",
+ "pendingOpTaskId": "المهمة={id}",
+ "pendingOpAttempts": "المحاولات={count}",
"retry": "إعادة المحاولة",
"discard": "تجاهل",
"discardChangesAction": "تجاهل التغييرات",
@@ -346,11 +347,11 @@
"discardPendingOperationConfirmation": "سيؤدي ذلك إلى إزالة العملية المحلية المحظورة. ستُحدّث البيانات من Google Tasks في المزامنة التالية.",
"pendingOperationDiscarded": "تم تجاهل العملية المعلّقة.",
"syncFailureNotificationTitle": "فشلت مزامنة BusyMax",
- "syncFailureNotificationBody": "فشلت المزامنة في الخلفية. \u2068{message}\u2069",
+ "syncFailureNotificationBody": "فشلت المزامنة في الخلفية. {message}",
"conflictNotificationTitle": "تعارض في مزامنة BusyMax",
- "conflictNotificationBody": "تم حظر تغيير محلي معلّق. \u2068{summary}\u2069",
+ "conflictNotificationBody": "تم حظر تغيير محلي معلّق. {summary}",
"dueTodayNotificationTitle": "المهام المستحقة اليوم",
- "dueTodayNotificationBody": "{count, plural, =0{لا توجد مهام مستحقة اليوم.} =1{هناك مهمة واحدة مستحقة اليوم.} =2{هناك مهمتان مستحقتان اليوم.} few{هناك \u2068{count}\u2069 مهام مستحقة اليوم.} many{هناك \u2068{count}\u2069 مهمة مستحقة اليوم.} other{هناك \u2068{count}\u2069 مهمة مستحقة اليوم.}}",
+ "dueTodayNotificationBody": "{count, plural, =0{لا توجد مهام مستحقة اليوم.} =1{هناك مهمة واحدة مستحقة اليوم.} =2{هناك مهمتان مستحقتان اليوم.} few{هناك {count} مهام مستحقة اليوم.} many{هناك {count} مهمة مستحقة اليوم.} other{هناك {count} مهمة مستحقة اليوم.}}",
"eventReminderNotificationTitle": "تذكير بحدث",
"taskReminderNotificationTitle": "تذكير بمهمة",
"eventReminderNotificationBody": "سيبدأ الحدث قريبًا.",
@@ -363,12 +364,208 @@
"previousYear": "السنة السابقة",
"nextYear": "السنة التالية",
"openYearView": "فتح عرض السنة",
- "weekNumberTooltip": "الأسبوع \u2068{number}\u2069",
+ "weekNumberTooltip": "الأسبوع {number}",
"resizeAllDayPanel": "تغيير حجم لوحة اليوم الكامل",
- "scheduleItemCount": "{count, plural, =0{لا عناصر} =1{عنصر واحد} =2{عنصران} few{\u2068{count}\u2069 عناصر} many{\u2068{count}\u2069 عنصرًا} other{\u2068{count}\u2069 عنصر}}",
+ "scheduleItemCount": "{count, plural, =0{لا عناصر} =1{عنصر واحد} =2{عنصران} few{{count} عناصر} many{{count} عنصرًا} other{{count} عنصر}}",
"readOnlyCalendar": "هذا التقويم للقراءة فقط.",
"selectTimeZone": "اختيار المنطقة الزمنية",
"searchLocations": "البحث عن مواقع",
"noLocationsFound": "لم يتم العثور على مواقع",
- "deleteCalendarConfirmation": "حذف «\u2068{title}\u2069»؟"
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
+ "deleteCalendarConfirmation": "حذف «{title}»؟",
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {
+ "placeholders": {
+ "host": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {
+ "placeholders": {
+ "code": {
+ "type": "String"
+ }
+ }
+ },
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "يجب ألا يكون موعد الاستحقاق قبل وقت البدء.",
+ "taskStartDueTimeModeMismatch": "عيّن وقتًا لكل من البدء والاستحقاق، أو اجعل المهمة طوال اليوم.",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "unshare": "إلغاء المشاركة",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "تحتاج إلى إجراء",
+ "taskStatusInProcess": "تحت الإجراء",
+ "taskStatusCompleted": "مُكتمِل",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "completionDate": "Completion date",
+ "priority": "الأولوية",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "priorityLowValue": "Priority {priority} · Low",
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
+ "reminders": "Reminders",
+ "noReminders": "لا توجد أي تذكيرات",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "قبل أن تبدأ المهمة",
+ "beforeTaskDue": "قبل اكتمال المهمة",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "discardRelatedReminders": "Discard reminders",
+ "keepRelatedReminders": "Keep reminders",
+ "repeatEvery": "تكرار كل",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "نهاية التكرار",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "repeatEveryWeeks": "Every {count} weeks",
+ "repeatEveryMonths": "Every {count} months",
+ "repeatEveryYears": "Every {count} years",
+ "repeatOnDaysSummary": "on {days}",
+ "repeatOnMonthDaysSummary": "في يوم {days}",
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "repeatInMonthsSummary": "in {months}",
+ "repeatTimesSummary": "{count} مرات",
+ "repeatUntilSummary": "until {date}",
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "duplicateTask": "Duplicate task",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "hideSubtasks": "إخْفِ المهام الفرعية",
+ "hideClosedSubtasks": "إخف المهام الفرعية المغلقة",
+ "reminderUnit": "Unit"
}
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
index dd8a9bb..cab9c91 100644
--- a/lib/l10n/app_de.arb
+++ b/lib/l10n/app_de.arb
@@ -1,14 +1,14 @@
{
"@@locale": "de",
"appTitle": "BusyMax",
- "connectGoogleAccount": "Verbinden Sie Google- und Microsoft-Konten, um Kalender und Aufgaben zu synchronisieren.",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "Wählen Sie auf dem Google-Berechtigungsbildschirm sowohl Kalender- als auch Aufgabenberechtigungen aus.",
"googlePermissionsRequiredRetry": "Die Berechtigungen für Google Kalender und Google Tasks sind erforderlich. Versuchen Sie es erneut und aktivieren Sie beide Kontrollkästchen.",
"finishSetup": "Einrichtung abschließen",
"continueSetup": "Weiter",
"onboardingSetupTitle": "BusyMax einrichten",
"onboardingAccountsStepTitle": "Konten verbinden",
- "onboardingAccountsStepDescription": "Fügen Sie alle Google- und Microsoft-Konten hinzu, die Sie verwenden möchten. BusyMax synchronisiert Kalender, Termine, Aufgabenlisten und Aufgaben aus jedem Konto.",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "Systemeinstellungen wählen",
"onboardingPreferencesStepDescription": "Legen Sie Desktop-Verhalten, Erinnerungen, Benachrichtigungsdetails und Darstellung fest, bevor Sie Ihren Zeitplan öffnen.",
"signInWithGoogle": "Mit Google anmelden",
@@ -94,12 +94,30 @@
"formatUnderlineShortLabel": "U",
"formatUnderlineTooltip": "Unterstrichen",
"reminderMinutesBefore": "{minutes, plural, =1{1 Minute vorher} other{{minutes} Minuten vorher}}",
- "@reminderMinutesBefore": {"placeholders": {"minutes": {"type": "int"}}},
+ "@reminderMinutesBefore": {
+ "placeholders": {
+ "minutes": {
+ "type": "int"
+ }
+ }
+ },
"reminderAtStart": "Zum Startzeitpunkt",
"reminderHoursBefore": "{hours, plural, =1{1 Stunde vorher} other{{hours} Stunden vorher}}",
- "@reminderHoursBefore": {"placeholders": {"hours": {"type": "int"}}},
+ "@reminderHoursBefore": {
+ "placeholders": {
+ "hours": {
+ "type": "int"
+ }
+ }
+ },
"reminderDaysBefore": "{days, plural, =1{1 Tag vorher} other{{days} Tage vorher}}",
- "@reminderDaysBefore": {"placeholders": {"days": {"type": "int"}}},
+ "@reminderDaysBefore": {
+ "placeholders": {
+ "days": {
+ "type": "int"
+ }
+ }
+ },
"availabilityFree": "Frei",
"availabilityTentative": "Mit Vorbehalt",
"availabilityOutOfOffice": "Abwesend",
@@ -183,14 +201,20 @@
"removingAccount": "Konto wird entfernt…",
"removeAccountDescription": "Synchronisierung beenden und die Daten dieses Kontos von diesem Gerät entfernen.",
"removeAccountTitle": "{account} aus BusyMax entfernen?",
- "@removeAccountTitle": {"placeholders": {"account": {"type": "String"}}},
- "removeAccountConfirmation": "Dadurch werden zwischengespeicherte Aufgaben, Kalender, Termine, Erinnerungen und ausstehende Offline-Änderungen von diesem Gerät gelöscht. Nicht synchronisierte Änderungen gehen verloren. Bei Google oder Microsoft wird nichts gelöscht.",
+ "@removeAccountTitle": {
+ "placeholders": {
+ "account": {
+ "type": "String"
+ }
+ }
+ },
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "BusyMax-Zugriff auf dieses Google-Konto ebenfalls widerrufen",
"revokeGoogleAccessDescription": "Vor einer erneuten Verbindung müssen Sie den Zugriff wieder gewähren.",
"removeAccountAction": "Konto entfernen",
"removeAccountFailed": "Das Konto konnte nicht vollständig entfernt werden. Versuchen Sie es erneut.",
"accountRemovedGoogleRevokeFailed": "Das Konto wurde von diesem Gerät entfernt, aber BusyMax konnte den Google-Zugriff nicht widerrufen. Sie können ihn in Ihrem Google-Konto widerrufen.",
- "newList": "Neue Liste",
+ "newTaskList": "Neue Aufgabenliste",
"signInToViewTaskLists": "Melden Sie sich an, um Aufgabenlisten zu sehen.",
"noTaskListsSynced": "Noch keine Aufgabenlisten synchronisiert.",
"listActions": "Listenaktionen",
@@ -211,9 +235,21 @@
"listRefreshed": "Liste aktualisiert.",
"allTasksRefreshed": "Alle Konten aktualisiert.",
"exportedFile": "Exportiert nach {path}",
- "@exportedFile": {"placeholders": {"path": {"type": "String"}}},
+ "@exportedFile": {
+ "placeholders": {
+ "path": {
+ "type": "String"
+ }
+ }
+ },
"exportFailed": "Export fehlgeschlagen: {error}",
- "@exportFailed": {"placeholders": {"error": {"type": "String"}}},
+ "@exportFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
"refreshFailed": "Aktualisierung fehlgeschlagen: {error}",
"selectOrCreateTaskList": "Wählen oder erstellen Sie zunächst eine Aufgabenliste.",
"signInToViewTasks": "Melden Sie sich an, um Aufgaben zu sehen.",
@@ -279,10 +315,11 @@
"list": "Liste",
"microsoftMoveUnsupported": "Das Verschieben zwischen Listen wird für Microsoft To Do-Konten in dieser Version nicht unterstützt.",
"createSubtask": "Unteraufgabe erstellen",
+ "subtasks": "Unteraufgaben",
"moveToTop": "Ganz nach oben verschieben",
"deleteTask": "Aufgabe löschen",
"newSubtask": "Neue Unteraufgabe",
- "deleteTaskConfirmation": "\"{title}\" aus Google Tasks löschen?",
+ "deleteTaskConfirmation": "\"{title}\" löschen?",
"metadata": "Metadaten",
"id": "ID",
"etag": "ETag",
@@ -370,14 +407,228 @@
"nextYear": "Nächstes Jahr",
"openYearView": "Jahresansicht öffnen",
"weekNumberTooltip": "Woche {number}",
- "@weekNumberTooltip": {"placeholders": {"number": {"type": "int"}}},
+ "@weekNumberTooltip": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ }
+ }
+ },
"resizeAllDayPanel": "Ganztägigen Bereich vergrößern oder verkleinern",
"scheduleItemCount": "{count, plural, =1{1 Eintrag} other{{count} Einträge}}",
- "@scheduleItemCount": {"placeholders": {"count": {"type": "int"}}},
+ "@scheduleItemCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
"readOnlyCalendar": "Dieser Kalender ist schreibgeschützt.",
"selectTimeZone": "Zeitzone auswählen",
"searchLocations": "Orte suchen",
"noLocationsFound": "Keine Orte gefunden",
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
"deleteCalendarConfirmation": "\"{title}\" löschen?",
- "@deleteCalendarConfirmation": {"placeholders": {"title": {"type": "String"}}}
+ "@deleteCalendarConfirmation": {
+ "placeholders": {
+ "title": {
+ "type": "String"
+ }
+ }
+ },
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {
+ "placeholders": {
+ "host": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {
+ "placeholders": {
+ "code": {
+ "type": "String"
+ }
+ }
+ },
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "Die Fälligkeit darf nicht vor dem Beginn liegen.",
+ "taskStartDueTimeModeMismatch": "Lege für Beginn und Fälligkeit jeweils eine Uhrzeit fest oder mache die Aufgabe ganztägig.",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "unshare": "Freigabe aufheben",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "Handlungsbedarf",
+ "taskStatusInProcess": "In Bearbeitung",
+ "taskStatusCompleted": "Fertiggestellt",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "completionDate": "Completion date",
+ "priority": "Priorität",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "priorityLowValue": "Priority {priority} · Low",
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
+ "reminders": "Reminders",
+ "noReminders": "Keine Erinnerungen",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "Bevor die Aufgabe startet",
+ "beforeTaskDue": "Bevor die Aufgabe fällig ist",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "discardRelatedReminders": "Erinnerungen verwerfen",
+ "keepRelatedReminders": "Erinnerungen behalten",
+ "repeatEvery": "Wiederhole jeden",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "Wiederholung beenden",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "repeatEveryWeeks": "Every {count} weeks",
+ "repeatEveryMonths": "Every {count} months",
+ "repeatEveryYears": "Every {count} years",
+ "repeatOnDaysSummary": "an {days}",
+ "repeatOnMonthDaysSummary": "am Tag {days}",
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "repeatInMonthsSummary": "in {months}",
+ "repeatTimesSummary": "{count} mal",
+ "repeatUntilSummary": "bis {date}",
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "duplicateTask": "Aufgabe duplizieren",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "hideSubtasks": "Teilaufgaben ausblenden",
+ "hideClosedSubtasks": "Geschlossene Teilaufgaben ausblenden",
+ "reminderUnit": "Unit"
}
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index db12d05..51cd328 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -1,14 +1,14 @@
{
"@@locale": "en",
"appTitle": "BusyMax",
- "connectGoogleAccount": "Connect Google and Microsoft accounts to sync calendars and tasks.",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "On the Google permission screen, select both Calendar and Tasks permissions.",
"googlePermissionsRequiredRetry": "Google Calendar and Google Tasks permissions are required. Please try again and select both checkboxes.",
"finishSetup": "Finish setup",
"continueSetup": "Continue",
"onboardingSetupTitle": "Set Up BusyMax",
"onboardingAccountsStepTitle": "Connect accounts",
- "onboardingAccountsStepDescription": "Add all Google and Microsoft accounts you want to use. BusyMax syncs calendars, events, task lists, and tasks from each account.",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "Choose system settings",
"onboardingPreferencesStepDescription": "Set desktop behavior, reminders, notification detail, and appearance before opening your schedule.",
"signInWithGoogle": "Sign in with Google",
@@ -187,13 +187,19 @@
"removeAccountDescription": "Stop syncing and remove this account’s data from this device.",
"removeAccountTitle": "Remove {account} from BusyMax?",
"@removeAccountTitle": {"placeholders": {"account": {"type": "String"}}},
- "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Nothing will be deleted from Google or Microsoft.",
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "Also revoke BusyMax’s access to this Google Account",
"revokeGoogleAccessDescription": "You will need to grant access again before reconnecting.",
"removeAccountAction": "Remove account",
"removeAccountFailed": "Could not finish removing the account. Try again.",
"accountRemovedGoogleRevokeFailed": "The account was removed from this device, but BusyMax could not revoke Google access. You can revoke it from your Google Account.",
- "newList": "New list",
+ "newTaskList": "New task list",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "@taskListCreateFailed": {"placeholders": {"error": {"type": "String"}}},
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "@taskListRenameFailed": {"placeholders": {"error": {"type": "String"}}},
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "@taskListDeleteFailed": {"placeholders": {"error": {"type": "String"}}},
"signInToViewTaskLists": "Sign in to view task lists.",
"noTaskListsSynced": "No task lists synced yet.",
"listActions": "List actions",
@@ -201,10 +207,17 @@
"delete": "Delete",
"renameList": "Rename list",
"deleteList": "Delete list",
+ "unshare": "Unshare",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
"builtInMicrosoftList": "Built-in",
"builtInMicrosoftListCannotRenameDelete": "Built-in Microsoft To Do lists cannot be renamed or deleted.",
"deleteListConfirmation": "Delete \"{title}\" from Google Tasks?",
"@deleteListConfirmation": {"placeholders": {"title": {"type": "String"}}},
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "@deleteTaskListConfirmation": {"placeholders": {"title": {"type": "String"}}},
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "@unshareTaskListConfirmation": {"placeholders": {"title": {"type": "String"}}},
"deleteEvent": "Delete Event",
"title": "Title",
"create": "Create",
@@ -252,6 +265,30 @@
"statusSection": "Status",
"openStatus": "Open",
"doneStatus": "Done",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "Needs action",
+ "taskStatusInProcess": "In process",
+ "taskStatusCompleted": "Completed",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "@completionPercent": {"placeholders": {"percent": {"type": "int"}}},
+ "completionDate": "Completion date",
+ "priority": "Priority",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "@priorityHighValue": {"placeholders": {"priority": {"type": "int"}}},
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "@priorityMediumValue": {"placeholders": {"priority": {"type": "int"}}},
+ "priorityLowValue": "Priority {priority} · Low",
+ "@priorityLowValue": {"placeholders": {"priority": {"type": "int"}}},
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
"notes": "Notes",
"dueDate": "Due date",
"clearDueDate": "Clear due date",
@@ -264,6 +301,32 @@
"reminderTime": "Reminder time",
"reminder": "Reminder",
"addReminder": "Add Reminder",
+ "reminders": "Reminders",
+ "noReminders": "No reminders",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "Before the task starts",
+ "beforeTaskDue": "Before the task is due",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnit": "Unit",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "@relatedRemindersDescription": {"placeholders": {"count": {"type": "int"}}},
+ "discardRelatedReminders": "Discard reminders",
+ "keepRelatedReminders": "Keep reminders",
"addGuest": "Add Guest",
"addGuestEmail": "Add guest email",
"removeReminder": "Remove reminder",
@@ -275,6 +338,48 @@
"repeatWeekly": "Weekly",
"repeatMonthly": "Monthly",
"repeatYearly": "Yearly",
+ "repeatEvery": "Repeat every",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "End repeat",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "@repeatEveryDays": {"placeholders": {"count": {"type": "int"}}},
+ "repeatEveryWeeks": "Every {count} weeks",
+ "@repeatEveryWeeks": {"placeholders": {"count": {"type": "int"}}},
+ "repeatEveryMonths": "Every {count} months",
+ "@repeatEveryMonths": {"placeholders": {"count": {"type": "int"}}},
+ "repeatEveryYears": "Every {count} years",
+ "@repeatEveryYears": {"placeholders": {"count": {"type": "int"}}},
+ "repeatOnDaysSummary": "on {days}",
+ "@repeatOnDaysSummary": {"placeholders": {"days": {"type": "String"}}},
+ "repeatOnMonthDaysSummary": "on day {days}",
+ "@repeatOnMonthDaysSummary": {"placeholders": {"days": {"type": "String"}}},
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "@repeatOnOrdinalSummary": {"placeholders": {"ordinal": {"type": "String"}, "days": {"type": "String"}}},
+ "repeatInMonthsSummary": "in {months}",
+ "@repeatInMonthsSummary": {"placeholders": {"months": {"type": "String"}}},
+ "repeatTimesSummary": "{count} times",
+ "@repeatTimesSummary": {"placeholders": {"count": {"type": "int"}}},
+ "repeatUntilSummary": "until {date}",
+ "@repeatUntilSummary": {"placeholders": {"date": {"type": "String"}}},
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
"importance": "Importance",
"importanceLow": "Low",
"importanceNormal": "Normal",
@@ -291,10 +396,17 @@
"list": "List",
"microsoftMoveUnsupported": "Moving between lists is not supported for Microsoft To Do accounts in this version.",
"createSubtask": "Create subtask",
+ "subtasks": "Subtasks",
+ "duplicateTask": "Duplicate task",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "@taskDuplicateFailed": {"placeholders": {"error": {"type": "String"}}},
+ "hideSubtasks": "Hide subtasks",
+ "hideClosedSubtasks": "Hide closed subtasks",
"moveToTop": "Move to top",
"deleteTask": "Delete Task",
"newSubtask": "New subtask",
- "deleteTaskConfirmation": "Delete \"{title}\" from Google Tasks?",
+ "deleteTaskConfirmation": "Delete \"{title}\"?",
"@deleteTaskConfirmation": {"placeholders": {"title": {"type": "String"}}},
"metadata": "Metadata",
"id": "ID",
@@ -402,6 +514,76 @@
"selectTimeZone": "Select Timezone",
"searchLocations": "Search locations",
"noLocationsFound": "No locations found",
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {"placeholders": {"time": {"type": "String"}}},
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {"placeholders": {"host": {"type": "String"}}},
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {"placeholders": {"time": {"type": "String"}}},
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {"placeholders": {"code": {"type": "String"}}},
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {"placeholders": {"time": {"type": "String"}}},
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {"placeholders": {"summary": {"type": "String"}}},
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "Due must not be before start.",
+ "taskStartDueTimeModeMismatch": "Set times for both start and due, or make the task all day.",
"deleteCalendarConfirmation": "Delete \"{title}\"?",
"@deleteCalendarConfirmation": {"placeholders": {"title": {"type": "String"}}}
}
diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb
index cfd3fef..8c2ee64 100644
--- a/lib/l10n/app_es.arb
+++ b/lib/l10n/app_es.arb
@@ -1,14 +1,14 @@
{
"@@locale": "es",
"appTitle": "BusyMax",
- "connectGoogleAccount": "Conecta cuentas de Google y Microsoft para sincronizar calendarios y tareas.",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "En la pantalla de permisos de Google, selecciona los permisos de Calendario y Tareas.",
"googlePermissionsRequiredRetry": "Los permisos de Google Calendar y Google Tasks son obligatorios. Inténtalo de nuevo y selecciona ambas casillas.",
"finishSetup": "Finalizar configuración",
"continueSetup": "Continuar",
"onboardingSetupTitle": "Configurar BusyMax",
"onboardingAccountsStepTitle": "Conectar cuentas",
- "onboardingAccountsStepDescription": "Añade todas las cuentas de Google y Microsoft que quieras usar. BusyMax sincroniza calendarios, eventos, listas de tareas y tareas de cada cuenta.",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "Elegir ajustes del sistema",
"onboardingPreferencesStepDescription": "Configura el comportamiento de la aplicación en el escritorio, los recordatorios, el nivel de detalle de las notificaciones y la apariencia antes de abrir tu agenda.",
"signInWithGoogle": "Iniciar sesión con Google",
@@ -94,12 +94,30 @@
"formatUnderlineShortLabel": "S",
"formatUnderlineTooltip": "Subrayado",
"reminderMinutesBefore": "{minutes, plural, =1{1 minuto antes} other{{minutes} minutos antes}}",
- "@reminderMinutesBefore": {"placeholders": {"minutes": {"type": "int"}}},
+ "@reminderMinutesBefore": {
+ "placeholders": {
+ "minutes": {
+ "type": "int"
+ }
+ }
+ },
"reminderAtStart": "A la hora de inicio",
"reminderHoursBefore": "{hours, plural, =1{1 hora antes} other{{hours} horas antes}}",
- "@reminderHoursBefore": {"placeholders": {"hours": {"type": "int"}}},
+ "@reminderHoursBefore": {
+ "placeholders": {
+ "hours": {
+ "type": "int"
+ }
+ }
+ },
"reminderDaysBefore": "{days, plural, =1{1 día antes} other{{days} días antes}}",
- "@reminderDaysBefore": {"placeholders": {"days": {"type": "int"}}},
+ "@reminderDaysBefore": {
+ "placeholders": {
+ "days": {
+ "type": "int"
+ }
+ }
+ },
"availabilityFree": "Libre",
"availabilityTentative": "Provisional",
"availabilityOutOfOffice": "Fuera de la oficina",
@@ -183,14 +201,20 @@
"removingAccount": "Eliminando cuenta…",
"removeAccountDescription": "Detener la sincronización y eliminar de este dispositivo los datos de esta cuenta.",
"removeAccountTitle": "¿Eliminar {account} de BusyMax?",
- "@removeAccountTitle": {"placeholders": {"account": {"type": "String"}}},
- "removeAccountConfirmation": "Esto elimina de este dispositivo las tareas, los calendarios, los eventos, los recordatorios y los cambios sin conexión pendientes almacenados en caché. Los cambios no sincronizados se perderán. No se eliminará nada de Google ni Microsoft.",
+ "@removeAccountTitle": {
+ "placeholders": {
+ "account": {
+ "type": "String"
+ }
+ }
+ },
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "Revocar también el acceso de BusyMax a esta cuenta de Google",
"revokeGoogleAccessDescription": "Tendrás que volver a conceder acceso antes de reconectar la cuenta.",
"removeAccountAction": "Eliminar cuenta",
"removeAccountFailed": "No se pudo terminar de eliminar la cuenta. Inténtalo de nuevo.",
"accountRemovedGoogleRevokeFailed": "La cuenta se eliminó de este dispositivo, pero BusyMax no pudo revocar su acceso a tu cuenta de Google. Puedes revocarlo desde tu cuenta de Google.",
- "newList": "Nueva lista",
+ "newTaskList": "Nueva lista de tareas",
"signInToViewTaskLists": "Inicia sesión para ver las listas de tareas.",
"noTaskListsSynced": "Aún no hay listas de tareas sincronizadas.",
"listActions": "Acciones de lista",
@@ -211,9 +235,21 @@
"listRefreshed": "Lista actualizada.",
"allTasksRefreshed": "Todas las cuentas se actualizaron.",
"exportedFile": "Exportado a {path}",
- "@exportedFile": {"placeholders": {"path": {"type": "String"}}},
+ "@exportedFile": {
+ "placeholders": {
+ "path": {
+ "type": "String"
+ }
+ }
+ },
"exportFailed": "Error al exportar: {error}",
- "@exportFailed": {"placeholders": {"error": {"type": "String"}}},
+ "@exportFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
"refreshFailed": "Error al actualizar: {error}",
"selectOrCreateTaskList": "Selecciona o crea una lista de tareas para empezar.",
"signInToViewTasks": "Inicia sesión para ver las tareas.",
@@ -279,10 +315,11 @@
"list": "Lista",
"microsoftMoveUnsupported": "En esta versión, no se pueden mover tareas entre listas en cuentas de Microsoft To Do.",
"createSubtask": "Crear subtarea",
+ "subtasks": "Subtareas",
"moveToTop": "Mover al principio",
"deleteTask": "Eliminar tarea",
"newSubtask": "Nueva subtarea",
- "deleteTaskConfirmation": "¿Eliminar \"{title}\" de Google Tasks?",
+ "deleteTaskConfirmation": "¿Eliminar \"{title}\"?",
"metadata": "Metadatos",
"id": "ID",
"etag": "ETag",
@@ -370,14 +407,228 @@
"nextYear": "Año siguiente",
"openYearView": "Abrir la vista anual",
"weekNumberTooltip": "Semana {number}",
- "@weekNumberTooltip": {"placeholders": {"number": {"type": "int"}}},
+ "@weekNumberTooltip": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ }
+ }
+ },
"resizeAllDayPanel": "Cambiar el tamaño del panel de todo el día",
"scheduleItemCount": "{count, plural, =1{1 elemento} other{{count} elementos}}",
- "@scheduleItemCount": {"placeholders": {"count": {"type": "int"}}},
+ "@scheduleItemCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
"readOnlyCalendar": "Este calendario es de solo lectura.",
"selectTimeZone": "Seleccionar zona horaria",
"searchLocations": "Buscar ubicaciones",
"noLocationsFound": "No se encontraron ubicaciones",
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
"deleteCalendarConfirmation": "¿Eliminar \"{title}\"?",
- "@deleteCalendarConfirmation": {"placeholders": {"title": {"type": "String"}}}
+ "@deleteCalendarConfirmation": {
+ "placeholders": {
+ "title": {
+ "type": "String"
+ }
+ }
+ },
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {
+ "placeholders": {
+ "host": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {
+ "placeholders": {
+ "code": {
+ "type": "String"
+ }
+ }
+ },
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "El vencimiento no puede ser anterior al inicio.",
+ "taskStartDueTimeModeMismatch": "Define una hora tanto para el inicio como para el vencimiento, o configura la tarea para todo el día.",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "unshare": "No compartir",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "Necesita una acción",
+ "taskStatusInProcess": "En proceso",
+ "taskStatusCompleted": "Completada",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "completionDate": "Completion date",
+ "priority": "Prioridad",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "priorityLowValue": "Priority {priority} · Low",
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
+ "reminders": "Reminders",
+ "noReminders": "Sin recordatorio",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "Antes de empezar la tarea",
+ "beforeTaskDue": "Antes de terminar la tarea",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "discardRelatedReminders": "Descartar recordatorios",
+ "keepRelatedReminders": "Mantener recordatorios",
+ "repeatEvery": "Repetir cada",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "Finalizar repetición",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "repeatEveryWeeks": "Every {count} weeks",
+ "repeatEveryMonths": "Every {count} months",
+ "repeatEveryYears": "Every {count} years",
+ "repeatOnDaysSummary": "dentro de {days}",
+ "repeatOnMonthDaysSummary": "en día {days}",
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "repeatInMonthsSummary": "en {months}",
+ "repeatTimesSummary": "{count} veces",
+ "repeatUntilSummary": "hasta el {date}",
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "duplicateTask": "Duplicar tarea",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "hideSubtasks": "Ocultar subtareas",
+ "hideClosedSubtasks": "Ocultar subtareas cerradas",
+ "reminderUnit": "Unit"
}
diff --git a/lib/l10n/app_et.arb b/lib/l10n/app_et.arb
index 6b952dc..b191fb7 100644
--- a/lib/l10n/app_et.arb
+++ b/lib/l10n/app_et.arb
@@ -1,14 +1,14 @@
{
"@@locale": "et",
"appTitle": "BusyMax",
- "connectGoogleAccount": "Ühendage Google'i ja Microsofti kontod kalendrite ja ülesannete sünkroonimiseks.",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "Valige Google'i õiguste kuval nii kalendri kui ka ülesannete õigused.",
"googlePermissionsRequiredRetry": "Google Calendari ja Google Tasksi õigused on nõutavad. Proovige uuesti ja märkige mõlemad ruudud.",
"finishSetup": "Lõpeta seadistamine",
"continueSetup": "Jätka",
"onboardingSetupTitle": "BusyMaxi seadistamine",
"onboardingAccountsStepTitle": "Kontode ühendamine",
- "onboardingAccountsStepDescription": "Lisage kõik Google'i ja Microsofti kontod, mida soovite kasutada. BusyMax sünkroonib iga konto kalendrid, sündmused, ülesandeloendid ja ülesanded.",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "Süsteemiseadete valimine",
"onboardingPreferencesStepDescription": "Enne ajakava avamist määrake töölauakäitumine, meeldetuletused, teavituste üksikasjalikkus ja välimus.",
"signInWithGoogle": "Logi Google'iga sisse",
@@ -41,7 +41,13 @@
"noCalendarsSynced": "Ühtegi kalendrit pole veel sünkroonitud.",
"allDay": "Kogu päev",
"moreItems": "+{count} veel",
- "@moreItems": {"placeholders": {"count": {"type": "int"}}},
+ "@moreItems": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
"noEventsOrTasks": "Sündmusi ega ülesandeid pole",
"scheduleLoading": "Ajakava laadimine...",
"scheduleUnavailable": "Ajakava pole saadaval",
@@ -95,12 +101,30 @@
"formatUnderlineShortLabel": "A",
"formatUnderlineTooltip": "Allajoonitud",
"reminderMinutesBefore": "{minutes, plural, =1{1 minut varem} other{{minutes} minutit varem}}",
- "@reminderMinutesBefore": {"placeholders": {"minutes": {"type": "int"}}},
+ "@reminderMinutesBefore": {
+ "placeholders": {
+ "minutes": {
+ "type": "int"
+ }
+ }
+ },
"reminderAtStart": "Algusajal",
"reminderHoursBefore": "{hours, plural, =1{1 tund varem} other{{hours} tundi varem}}",
- "@reminderHoursBefore": {"placeholders": {"hours": {"type": "int"}}},
+ "@reminderHoursBefore": {
+ "placeholders": {
+ "hours": {
+ "type": "int"
+ }
+ }
+ },
"reminderDaysBefore": "{days, plural, =1{1 päev varem} other{{days} päeva varem}}",
- "@reminderDaysBefore": {"placeholders": {"days": {"type": "int"}}},
+ "@reminderDaysBefore": {
+ "placeholders": {
+ "days": {
+ "type": "int"
+ }
+ }
+ },
"availabilityFree": "Vaba",
"availabilityTentative": "Esialgne",
"availabilityOutOfOffice": "Kontorist väljas",
@@ -114,7 +138,13 @@
"tasks": "Ülesanded",
"allTasks": "Kõik ülesanded",
"tasksInList": "Loendi „{title}” ülesanded",
- "@tasksInList": {"placeholders": {"title": {"type": "String"}}},
+ "@tasksInList": {
+ "placeholders": {
+ "title": {
+ "type": "String"
+ }
+ }
+ },
"taskLists": "Ülesandeloendid",
"navigation": "Navigeerimine",
"mainMenu": "Peamenüü",
@@ -170,7 +200,13 @@
"feedbackRejectedError": "Server lükkas saatmise tagasi. Kontrollige välju ja proovige uuesti.",
"feedbackServerError": "BusyStack ei saa praegu teie tagasisidet vastu võtta. Teie tagasisidet ei kustutatud; proovige uuesti.",
"feedbackSuccess": "Tagasiside saadetud. Viide: {id}",
- "@feedbackSuccess": {"placeholders": {"id": {"type": "String"}}},
+ "@feedbackSuccess": {
+ "placeholders": {
+ "id": {
+ "type": "String"
+ }
+ }
+ },
"toggleSidebar": "Kuva või peida külgriba",
"showSidebar": "Kuva külgpaneel",
"hideSidebar": "Peida külgpaneel",
@@ -186,14 +222,20 @@
"removingAccount": "Konto eemaldamine…",
"removeAccountDescription": "Lõpeta sünkroonimine ja eemalda selle konto andmed seadmest.",
"removeAccountTitle": "Kas eemaldada {account} BusyMaxist?",
- "@removeAccountTitle": {"placeholders": {"account": {"type": "String"}}},
- "removeAccountConfirmation": "See kustutab seadmest vahemällu salvestatud ülesanded, kalendrid, sündmused, meeldetuletused ja sünkroonimist ootavad võrguühenduseta muudatused. Sünkroonimata muudatused lähevad kaotsi. Google'ist ega Microsoftist midagi ei kustutata.",
+ "@removeAccountTitle": {
+ "placeholders": {
+ "account": {
+ "type": "String"
+ }
+ }
+ },
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "Tühista ka BusyMaxi juurdepääs sellele Google'i kontole",
"revokeGoogleAccessDescription": "Enne uuesti ühendamist peate juurdepääsu uuesti andma.",
"removeAccountAction": "Eemalda konto",
"removeAccountFailed": "Konto eemaldamist ei saanud lõpetada. Proovige uuesti.",
"accountRemovedGoogleRevokeFailed": "Konto eemaldati sellest seadmest, kuid BusyMaxi juurdepääsu teie Google'i kontole ei saanud tühistada. Saate selle oma Google'i kontol käsitsi tühistada.",
- "newList": "Uus loend",
+ "newTaskList": "Uus ülesandeloend",
"signInToViewTaskLists": "Ülesandeloendite vaatamiseks logige sisse.",
"noTaskListsSynced": "Ühtegi ülesandeloendit pole veel sünkroonitud.",
"listActions": "Loendi toimingud",
@@ -204,7 +246,13 @@
"builtInMicrosoftList": "Sisseehitatud",
"builtInMicrosoftListCannotRenameDelete": "Microsoft To Do sisseehitatud loendeid ei saa ümber nimetada ega kustutada.",
"deleteListConfirmation": "Kas kustutada „{title}” Google Tasksist?",
- "@deleteListConfirmation": {"placeholders": {"title": {"type": "String"}}},
+ "@deleteListConfirmation": {
+ "placeholders": {
+ "title": {
+ "type": "String"
+ }
+ }
+ },
"deleteEvent": "Kustuta sündmus",
"title": "Pealkiri",
"create": "Loo",
@@ -215,11 +263,29 @@
"listRefreshed": "Loend on värskendatud.",
"allTasksRefreshed": "Kõik kontod on värskendatud.",
"exportedFile": "Eksporditud asukohta {path}",
- "@exportedFile": {"placeholders": {"path": {"type": "String"}}},
+ "@exportedFile": {
+ "placeholders": {
+ "path": {
+ "type": "String"
+ }
+ }
+ },
"exportFailed": "Eksportimine nurjus: {error}",
- "@exportFailed": {"placeholders": {"error": {"type": "String"}}},
+ "@exportFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
"refreshFailed": "Värskendamine nurjus: {error}",
- "@refreshFailed": {"placeholders": {"error": {"type": "String"}}},
+ "@refreshFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
"selectOrCreateTaskList": "Alustamiseks valige või looge ülesandeloend.",
"signInToViewTasks": "Ülesannete vaatamiseks logige sisse.",
"noTasks": "Ülesandeid pole.",
@@ -233,12 +299,22 @@
"noDate": "Kuupäevata",
"completed": "Lõpetatud",
"duePrefix": "Tähtaeg {date}",
- "@duePrefix": {"placeholders": {"date": {"type": "String"}}},
+ "@duePrefix": {
+ "placeholders": {
+ "date": {
+ "type": "String"
+ }
+ }
+ },
"dateTimeDisplay": "{date} · {time}",
"@dateTimeDisplay": {
"placeholders": {
- "date": {"type": "String"},
- "time": {"type": "String"}
+ "date": {
+ "type": "String"
+ },
+ "time": {
+ "type": "String"
+ }
}
},
"taskDetails": "Ülesande üksikasjad",
@@ -291,11 +367,18 @@
"list": "Loend",
"microsoftMoveUnsupported": "Selles versioonis ei toetata Microsoft To Do kontodel ülesannete teisaldamist loendite vahel.",
"createSubtask": "Loo alamülesanne",
+ "subtasks": "Alamülesanded",
"moveToTop": "Teisalda kõige üles",
"deleteTask": "Kustuta ülesanne",
"newSubtask": "Uus alamülesanne",
- "deleteTaskConfirmation": "Kas kustutada „{title}” Google Tasksist?",
- "@deleteTaskConfirmation": {"placeholders": {"title": {"type": "String"}}},
+ "deleteTaskConfirmation": "Kas kustutada „{title}”?",
+ "@deleteTaskConfirmation": {
+ "placeholders": {
+ "title": {
+ "type": "String"
+ }
+ }
+ },
"metadata": "Metaandmed",
"id": "ID",
"etag": "ETag",
@@ -316,7 +399,13 @@
"requiresTrayIcon": "Nõuab süsteemisalve ikooni.",
"syncComplete": "Sünkroonimine on lõpetatud.",
"syncFailed": "Sünkroonimine nurjus: {error}",
- "@syncFailed": {"placeholders": {"error": {"type": "String"}}},
+ "@syncFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
"notifySyncFailures": "Teavitused sünkroonimise nurjumisel",
"notifyConflicts": "Teavitused konfliktide korral",
"notifyDueToday": "Täna tähtuvate ülesannete teavitused",
@@ -346,7 +435,13 @@
"apiInspectorDisabled": "Kuva API-inspektor",
"googleTasksApi": "Google Tasks API",
"discoveryRevision": "Discovery versioon: {revision}",
- "@discoveryRevision": {"placeholders": {"revision": {"type": "String"}}},
+ "@discoveryRevision": {
+ "placeholders": {
+ "revision": {
+ "type": "String"
+ }
+ }
+ },
"implementedMethods": "Rakendatud meetodid",
"supportsTasksScopes": "Toetab õiguse ulatusi tasks ja tasks.readonly",
"requiresTasksScope": "Nõuab õiguse ulatust tasks",
@@ -355,11 +450,29 @@
"noBlockedPendingOperations": "Blokeeritud ootel toiminguid pole.",
"operationActions": "Toimingu tegevused",
"pendingOpListId": "loend={id}",
- "@pendingOpListId": {"placeholders": {"id": {"type": "String"}}},
+ "@pendingOpListId": {
+ "placeholders": {
+ "id": {
+ "type": "String"
+ }
+ }
+ },
"pendingOpTaskId": "ülesanne={id}",
- "@pendingOpTaskId": {"placeholders": {"id": {"type": "String"}}},
+ "@pendingOpTaskId": {
+ "placeholders": {
+ "id": {
+ "type": "String"
+ }
+ }
+ },
"pendingOpAttempts": "katseid={count}",
- "@pendingOpAttempts": {"placeholders": {"count": {"type": "int"}}},
+ "@pendingOpAttempts": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
"retry": "Proovi uuesti",
"discard": "Hülga",
"discardChangesAction": "Hülga",
@@ -371,13 +484,31 @@
"pendingOperationDiscarded": "Ootel toiming hüljatud.",
"syncFailureNotificationTitle": "BusyMaxi sünkroonimine nurjus",
"syncFailureNotificationBody": "Taustal sünkroonimine nurjus. {message}",
- "@syncFailureNotificationBody": {"placeholders": {"message": {"type": "String"}}},
+ "@syncFailureNotificationBody": {
+ "placeholders": {
+ "message": {
+ "type": "String"
+ }
+ }
+ },
"conflictNotificationTitle": "BusyMaxi sünkroonimiskonflikt",
"conflictNotificationBody": "Ootel kohalik muudatus blokeeriti. {summary}",
- "@conflictNotificationBody": {"placeholders": {"summary": {"type": "String"}}},
+ "@conflictNotificationBody": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
"dueTodayNotificationTitle": "Täna tähtuvad ülesanded",
"dueTodayNotificationBody": "{count, plural, =1{Üks ülesanne tähtub täna.} other{{count} ülesannet tähtub täna.}}",
- "@dueTodayNotificationBody": {"placeholders": {"count": {"type": "int"}}},
+ "@dueTodayNotificationBody": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
"eventReminderNotificationTitle": "Sündmuse meeldetuletus",
"taskReminderNotificationTitle": "Ülesande meeldetuletus",
"eventReminderNotificationBody": "Sündmus algab varsti.",
@@ -391,14 +522,228 @@
"nextYear": "Järgmine aasta",
"openYearView": "Ava aastavaade",
"weekNumberTooltip": "Nädal {number}",
- "@weekNumberTooltip": {"placeholders": {"number": {"type": "int"}}},
+ "@weekNumberTooltip": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ }
+ }
+ },
"resizeAllDayPanel": "Muuda kogu päeva paneeli suurust",
"scheduleItemCount": "{count, plural, =1{1 kirje} other{{count} kirjet}}",
- "@scheduleItemCount": {"placeholders": {"count": {"type": "int"}}},
+ "@scheduleItemCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
"readOnlyCalendar": "See kalender on kirjutuskaitstud.",
"selectTimeZone": "Valige ajavöönd",
"searchLocations": "Otsi asukohti",
"noLocationsFound": "Asukohti ei leitud",
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
"deleteCalendarConfirmation": "Kas kustutada „{title}”?",
- "@deleteCalendarConfirmation": {"placeholders": {"title": {"type": "String"}}}
+ "@deleteCalendarConfirmation": {
+ "placeholders": {
+ "title": {
+ "type": "String"
+ }
+ }
+ },
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {
+ "placeholders": {
+ "host": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {
+ "placeholders": {
+ "code": {
+ "type": "String"
+ }
+ }
+ },
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "Tähtaeg ei tohi olla enne algust.",
+ "taskStartDueTimeModeMismatch": "Määra nii algus- kui ka tähtaja kellaaeg või tee ülesanne kogu päeva ülesandeks.",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "unshare": "Unshare",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "Needs action",
+ "taskStatusInProcess": "In process",
+ "taskStatusCompleted": "Completed",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "completionDate": "Completion date",
+ "priority": "Priority",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "priorityLowValue": "Priority {priority} · Low",
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
+ "reminders": "Reminders",
+ "noReminders": "No reminders",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "Before the task starts",
+ "beforeTaskDue": "Before the task is due",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "discardRelatedReminders": "Discard reminders",
+ "keepRelatedReminders": "Keep reminders",
+ "repeatEvery": "Repeat every",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "End repeat",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "repeatEveryWeeks": "Every {count} weeks",
+ "repeatEveryMonths": "Every {count} months",
+ "repeatEveryYears": "Every {count} years",
+ "repeatOnDaysSummary": "on {days}",
+ "repeatOnMonthDaysSummary": "on day {days}",
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "repeatInMonthsSummary": "in {months}",
+ "repeatTimesSummary": "{count} times",
+ "repeatUntilSummary": "until {date}",
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "duplicateTask": "Duplicate task",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "hideSubtasks": "Hide subtasks",
+ "hideClosedSubtasks": "Hide closed subtasks",
+ "reminderUnit": "Unit"
}
diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb
index 7caa2aa..131624f 100644
--- a/lib/l10n/app_fa.arb
+++ b/lib/l10n/app_fa.arb
@@ -1,14 +1,14 @@
{
"@@locale": "fa",
"appTitle": "BusyMax",
- "connectGoogleAccount": "حسابهای Google و Microsoft را متصل کنید تا تقویمها و کارها همگام شوند.",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "در صفحهٔ مجوزهای Google، مجوزهای تقویم و کارها را هر دو انتخاب کنید.",
"googlePermissionsRequiredRetry": "مجوزهای Google Calendar و Google Tasks لازم هستند. دوباره تلاش کنید و هر دو کادر را علامت بزنید.",
"finishSetup": "پایان راهاندازی",
"continueSetup": "ادامه",
"onboardingSetupTitle": "راهاندازی BusyMax",
"onboardingAccountsStepTitle": "اتصال حسابها",
- "onboardingAccountsStepDescription": "همهٔ حسابهای Google و Microsoft موردنظرتان را اضافه کنید. BusyMax تقویمها، رویدادها، فهرستهای کار و کارهای هر حساب را همگام میکند.",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "انتخاب تنظیمات سیستم",
"onboardingPreferencesStepDescription": "پیش از باز کردن برنامه، رفتار برنامه روی میزکار، یادآورها، سطح جزئیات اعلانها و ظاهر را تنظیم کنید.",
"signInWithGoogle": "ورود با Google",
@@ -40,7 +40,7 @@
"showInSchedule": "نمایش در برنامه",
"noCalendarsSynced": "هنوز هیچ تقویمی همگام نشده است.",
"allDay": "تمام روز",
- "moreItems": "+\u2068{count}\u2069 مورد دیگر",
+ "moreItems": "+{count} مورد دیگر",
"noEventsOrTasks": "هیچ رویداد یا کاری وجود ندارد",
"scheduleLoading": "در حال بارگیری برنامه...",
"scheduleUnavailable": "برنامه در دسترس نیست",
@@ -93,10 +93,10 @@
"formatItalicTooltip": "مورب",
"formatUnderlineShortLabel": "U",
"formatUnderlineTooltip": "زیرخط",
- "reminderMinutesBefore": "{minutes, plural, =0{هنگام شروع} =1{یک دقیقه قبل} other{\u2068{minutes}\u2069 دقیقه قبل}}",
+ "reminderMinutesBefore": "{minutes, plural, =0{هنگام شروع} =1{یک دقیقه قبل} other{{minutes} دقیقه قبل}}",
"reminderAtStart": "هنگام شروع",
- "reminderHoursBefore": "{hours, plural, =0{هنگام شروع} =1{یک ساعت قبل} other{\u2068{hours}\u2069 ساعت قبل}}",
- "reminderDaysBefore": "{days, plural, =0{همان روز} =1{یک روز قبل} other{\u2068{days}\u2069 روز قبل}}",
+ "reminderHoursBefore": "{hours, plural, =0{هنگام شروع} =1{یک ساعت قبل} other{{hours} ساعت قبل}}",
+ "reminderDaysBefore": "{days, plural, =0{همان روز} =1{یک روز قبل} other{{days} روز قبل}}",
"availabilityFree": "آزاد",
"availabilityTentative": "احتمالی",
"availabilityOutOfOffice": "خارج از دفتر",
@@ -109,7 +109,7 @@
"sensitivityPersonal": "شخصی",
"tasks": "کارها",
"allTasks": "همهٔ کارها",
- "tasksInList": "کارهای \u2068{title}\u2069",
+ "tasksInList": "کارهای {title}",
"taskLists": "فهرستهای کار",
"navigation": "پیمایش",
"mainMenu": "منوی اصلی",
@@ -164,7 +164,7 @@
"feedbackRateLimitedError": "بازخوردهای بیش از حدی از این شبکه ارسال شده است. کمی صبر کنید و دوباره تلاش کنید.",
"feedbackRejectedError": "سرور ارسال را رد کرد. فیلدها را بررسی و دوباره تلاش کنید.",
"feedbackServerError": "BusyStack اکنون نمیتواند بازخورد شما را بپذیرد. بازخورد شما پاک نشده است؛ دوباره تلاش کنید.",
- "feedbackSuccess": "بازخورد ارسال شد. شناسهٔ پیگیری: \u2068{id}\u2069",
+ "feedbackSuccess": "بازخورد ارسال شد. شناسهٔ پیگیری: {id}",
"toggleSidebar": "نمایش یا پنهان کردن نوار کناری",
"showSidebar": "نمایش پنل کناری",
"hideSidebar": "پنهان کردن پنل کناری",
@@ -179,14 +179,14 @@
"removeAccount": "حذف حساب…",
"removingAccount": "در حال حذف حساب…",
"removeAccountDescription": "همگامسازی را متوقف و دادههای این حساب را از این دستگاه حذف کنید.",
- "removeAccountTitle": "حذف \u2068{account}\u2069 از BusyMax؟",
- "removeAccountConfirmation": "با این کار، کارها، تقویمها، رویدادها، یادآورها و تغییرات آفلاین در انتظار از حافظهٔ نهان این دستگاه حذف میشوند. تغییرات همگامنشده از دست میروند. هیچ چیزی از Google یا Microsoft حذف نمیشود.",
+ "removeAccountTitle": "حذف {account} از BusyMax؟",
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "دسترسی BusyMax به این حساب Google نیز لغو شود",
"revokeGoogleAccessDescription": "پیش از اتصال دوباره باید دسترسی را دوباره اعطا کنید.",
"removeAccountAction": "حذف حساب",
"removeAccountFailed": "حذف حساب کامل نشد. دوباره تلاش کنید.",
"accountRemovedGoogleRevokeFailed": "حساب از این دستگاه حذف شد، اما BusyMax نتوانست دسترسی Google را لغو کند. میتوانید آن را از حساب Google خود لغو کنید.",
- "newList": "فهرست جدید",
+ "newTaskList": "فهرست کار جدید",
"signInToViewTaskLists": "برای دیدن فهرستهای کار وارد شوید.",
"noTaskListsSynced": "هنوز هیچ فهرست کاری همگام نشده است.",
"listActions": "عملیات فهرست",
@@ -196,7 +196,7 @@
"deleteList": "حذف فهرست",
"builtInMicrosoftList": "داخلی",
"builtInMicrosoftListCannotRenameDelete": "فهرستهای داخلی Microsoft To Do را نمیتوان تغییر نام داد یا حذف کرد.",
- "deleteListConfirmation": "«\u2068{title}\u2069» از Google Tasks حذف شود؟",
+ "deleteListConfirmation": "«{title}» از Google Tasks حذف شود؟",
"deleteEvent": "حذف رویداد",
"title": "عنوان",
"create": "ایجاد",
@@ -206,9 +206,9 @@
"refreshAll": "تازهسازی همه",
"listRefreshed": "فهرست تازهسازی شد.",
"allTasksRefreshed": "همهٔ حسابها تازهسازی شدند.",
- "exportedFile": "در \u2068{path}\u2069 خروجی گرفته شد",
- "exportFailed": "خروجی گرفتن ناموفق بود: \u2068{error}\u2069",
- "refreshFailed": "تازهسازی ناموفق بود: \u2068{error}\u2069",
+ "exportedFile": "در {path} خروجی گرفته شد",
+ "exportFailed": "خروجی گرفتن ناموفق بود: {error}",
+ "refreshFailed": "تازهسازی ناموفق بود: {error}",
"selectOrCreateTaskList": "برای شروع، یک فهرست کار انتخاب یا ایجاد کنید.",
"signInToViewTasks": "برای دیدن کارها وارد شوید.",
"noTasks": "هیچ کاری وجود ندارد.",
@@ -221,8 +221,8 @@
"upcoming": "پیش رو",
"noDate": "بدون تاریخ",
"completed": "انجامشده",
- "duePrefix": "سررسید: \u2068{date}\u2069",
- "dateTimeDisplay": "\u2068{date}\u2069 · \u2068{time}\u2069",
+ "duePrefix": "سررسید: {date}",
+ "dateTimeDisplay": "{date} · {time}",
"taskDetails": "جزئیات کار",
"editTask": "ویرایش کار",
"noTaskSelected": "هیچ کاری انتخاب نشده است.",
@@ -273,10 +273,11 @@
"list": "فهرست",
"microsoftMoveUnsupported": "در این نسخه، جابهجایی کارها بین فهرستهای حساب Microsoft To Do پشتیبانی نمیشود.",
"createSubtask": "ایجاد زیرکار",
+ "subtasks": "زیرکارها",
"moveToTop": "انتقال به بالاترین جایگاه",
"deleteTask": "حذف کار",
"newSubtask": "زیرکار جدید",
- "deleteTaskConfirmation": "«\u2068{title}\u2069» از Google Tasks حذف شود؟",
+ "deleteTaskConfirmation": "«{title}» حذف شود؟",
"metadata": "فراداده",
"id": "شناسه",
"etag": "ETag",
@@ -296,7 +297,7 @@
"startMinimizedToTray": "شروع بهصورت کوچکشده در سینی سیستم",
"requiresTrayIcon": "به نماد سینی سیستم نیاز دارد.",
"syncComplete": "همگامسازی کامل شد.",
- "syncFailed": "همگامسازی ناموفق بود: \u2068{error}\u2069",
+ "syncFailed": "همگامسازی ناموفق بود: {error}",
"notifySyncFailures": "اعلان هنگام شکست همگامسازی",
"notifyConflicts": "اعلان هنگام تداخل",
"notifyDueToday": "اعلان کارهای دارای سررسید امروز",
@@ -325,7 +326,7 @@
"diagnostics": "اطلاعات تشخیصی",
"apiInspectorDisabled": "نمایش بازرس API",
"googleTasksApi": "رابط Google Tasks API",
- "discoveryRevision": "بازبینی Discovery: \u2068{revision}\u2069",
+ "discoveryRevision": "بازبینی Discovery: {revision}",
"implementedMethods": "روشهای پیادهسازیشده",
"supportsTasksScopes": "از محدودههای tasks و tasks.readonly پشتیبانی میکند",
"requiresTasksScope": "به محدودهٔ tasks نیاز دارد",
@@ -333,9 +334,9 @@
"signInToInspectPendingOperations": "برای بررسی عملیات در انتظار وارد شوید.",
"noBlockedPendingOperations": "هیچ عملیات در انتظار مسدودشدهای وجود ندارد.",
"operationActions": "اقدامات عملیات",
- "pendingOpListId": "فهرست=\u2068{id}\u2069",
- "pendingOpTaskId": "کار=\u2068{id}\u2069",
- "pendingOpAttempts": "تلاشها=\u2068{count}\u2069",
+ "pendingOpListId": "فهرست={id}",
+ "pendingOpTaskId": "کار={id}",
+ "pendingOpAttempts": "تلاشها={count}",
"retry": "تلاش دوباره",
"discard": "کنار گذاشتن",
"discardChangesAction": "ذخیره نشود",
@@ -346,11 +347,11 @@
"discardPendingOperationConfirmation": "با این کار عملیات محلی مسدودشده حذف میشود. در همگامسازی بعدی، دادهها از Google Tasks تازهسازی میشوند.",
"pendingOperationDiscarded": "عملیات در انتظار کنار گذاشته شد.",
"syncFailureNotificationTitle": "همگامسازی BusyMax ناموفق بود",
- "syncFailureNotificationBody": "همگامسازی پسزمینه ناموفق بود. \u2068{message}\u2069",
+ "syncFailureNotificationBody": "همگامسازی پسزمینه ناموفق بود. {message}",
"conflictNotificationTitle": "تداخل همگامسازی BusyMax",
- "conflictNotificationBody": "یک تغییر محلی در انتظار مسدود شد. \u2068{summary}\u2069",
+ "conflictNotificationBody": "یک تغییر محلی در انتظار مسدود شد. {summary}",
"dueTodayNotificationTitle": "کارهای دارای سررسید امروز",
- "dueTodayNotificationBody": "{count, plural, =0{امروز هیچ کاری سررسید ندارد.} =1{امروز یک کار سررسید دارد.} other{امروز \u2068{count}\u2069 کار سررسید دارند.}}",
+ "dueTodayNotificationBody": "{count, plural, =0{امروز هیچ کاری سررسید ندارد.} =1{امروز یک کار سررسید دارد.} other{امروز {count} کار سررسید دارند.}}",
"eventReminderNotificationTitle": "یادآور رویداد",
"taskReminderNotificationTitle": "یادآور کار",
"eventReminderNotificationBody": "رویداد بهزودی شروع میشود.",
@@ -363,14 +364,52 @@
"previousYear": "سال قبل",
"nextYear": "سال بعد",
"openYearView": "باز کردن نمای سال",
- "weekNumberTooltip": "هفتهٔ \u2068{number}\u2069",
+ "weekNumberTooltip": "هفتهٔ {number}",
"resizeAllDayPanel": "تغییر اندازهٔ پنل تمامروز",
- "scheduleItemCount": "{count, plural, =0{هیچ موردی} =1{یک مورد} other{\u2068{count}\u2069 مورد}}",
+ "scheduleItemCount": "{count, plural, =0{هیچ موردی} =1{یک مورد} other{{count} مورد}}",
"readOnlyCalendar": "این تقویم فقطخواندنی است.",
"selectTimeZone": "انتخاب منطقهٔ زمانی",
"searchLocations": "جستوجوی مکانها",
"noLocationsFound": "مکانی پیدا نشد",
- "deleteCalendarConfirmation": "«\u2068{title}\u2069» حذف شود؟",
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
+ "deleteCalendarConfirmation": "«{title}» حذف شود؟",
"@moreItems": {
"placeholders": {
"count": {
@@ -434,5 +473,163 @@
"format": "decimalPattern"
}
}
- }
+ },
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {
+ "placeholders": {
+ "host": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {
+ "placeholders": {
+ "code": {
+ "type": "String"
+ }
+ }
+ },
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "زمان سررسید نباید پیش از زمان شروع باشد.",
+ "taskStartDueTimeModeMismatch": "برای شروع و سررسید هر دو زمان تعیین کنید، یا کار را تمامروز کنید.",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "unshare": "لغو اشتراکگذاری",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "نیاز به اقدام",
+ "taskStatusInProcess": "در حال انجام",
+ "taskStatusCompleted": "انجامشده",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "completionDate": "Completion date",
+ "priority": "اولویت",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "priorityLowValue": "Priority {priority} · Low",
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
+ "reminders": "Reminders",
+ "noReminders": "بدون یادآوری",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "قبل از شروع کار",
+ "beforeTaskDue": "قبل از موعد انجام کار",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "discardRelatedReminders": "حذف یادآوریها",
+ "keepRelatedReminders": "نگهداشتن یادآوریها",
+ "repeatEvery": "تکرار هر",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "پایان تکرار",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "repeatEveryWeeks": "Every {count} weeks",
+ "repeatEveryMonths": "Every {count} months",
+ "repeatEveryYears": "Every {count} years",
+ "repeatOnDaysSummary": "در {days}",
+ "repeatOnMonthDaysSummary": "در روز {days}",
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "repeatInMonthsSummary": "در {months}",
+ "repeatTimesSummary": "{count} بار",
+ "repeatUntilSummary": "تا {date}",
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "duplicateTask": "تکراریسازی وظیفه",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "hideSubtasks": "مخفیسازی زیروظیفهها",
+ "hideClosedSubtasks": "مخفیسازی زیروظیفههای بستهشده",
+ "reminderUnit": "Unit"
}
diff --git a/lib/l10n/app_fi.arb b/lib/l10n/app_fi.arb
index 900bd8d..f3d749e 100644
--- a/lib/l10n/app_fi.arb
+++ b/lib/l10n/app_fi.arb
@@ -1,14 +1,14 @@
{
"@@locale": "fi",
"appTitle": "BusyMax",
- "connectGoogleAccount": "Yhdistä Google- ja Microsoft-tilit kalenterien ja tehtävien synkronointia varten.",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "Valitse Googlen käyttöoikeusnäkymässä sekä Kalenteri- että Tehtävät-käyttöoikeudet.",
"googlePermissionsRequiredRetry": "Google Kalenterin ja Google Tasksin käyttöoikeudet vaaditaan. Yritä uudelleen ja valitse molemmat valintaruudut.",
"finishSetup": "Viimeistele määritys",
"continueSetup": "Jatka",
"onboardingSetupTitle": "Määritä BusyMax",
"onboardingAccountsStepTitle": "Yhdistä tilit",
- "onboardingAccountsStepDescription": "Lisää kaikki haluamasi Google- ja Microsoft-tilit. BusyMax synkronoi kunkin tilin kalenterit, tapahtumat, tehtäväluettelot ja tehtävät.",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "Valitse järjestelmäasetukset",
"onboardingPreferencesStepDescription": "Määritä sovelluksen toiminta työpöydällä, muistutukset, ilmoitusten yksityiskohtaisuus ja ulkoasu ennen aikataulun avaamista.",
"signInWithGoogle": "Kirjaudu Google-tilillä",
@@ -180,13 +180,13 @@
"removingAccount": "Poistetaan tiliä…",
"removeAccountDescription": "Lopeta synkronointi ja poista tämän tilin tiedot tältä laitteelta.",
"removeAccountTitle": "Poistetaanko {account} BusyMaxista?",
- "removeAccountConfirmation": "Tämä poistaa välimuistissa olevat tehtävät, kalenterit, tapahtumat, muistutukset ja odottavat offline-muutokset tältä laitteelta. Synkronoimattomat muutokset menetetään. Mitään ei poisteta Googlesta tai Microsoftista.",
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "Peruuta myös BusyMaxin käyttöoikeus tähän Google-tiliin",
"revokeGoogleAccessDescription": "Käyttöoikeus on myönnettävä uudelleen ennen tilin yhdistämistä.",
"removeAccountAction": "Poista tili",
"removeAccountFailed": "Tilin poistamista ei voitu viimeistellä. Yritä uudelleen.",
"accountRemovedGoogleRevokeFailed": "Tili poistettiin tältä laitteelta, mutta BusyMax ei voinut peruuttaa Google-käyttöoikeutta. Voit peruuttaa sen Google-tililtäsi.",
- "newList": "Uusi luettelo",
+ "newTaskList": "Uusi tehtäväluettelo",
"signInToViewTaskLists": "Kirjaudu sisään nähdäksesi tehtäväluettelot.",
"noTaskListsSynced": "Tehtäväluetteloita ei ole vielä synkronoitu.",
"listActions": "Luettelon toiminnot",
@@ -273,10 +273,11 @@
"list": "Luettelo",
"microsoftMoveUnsupported": "Luettelosta toiseen siirtämistä ei tueta Microsoft To Do -tileillä tässä versiossa.",
"createSubtask": "Luo alitehtävä",
+ "subtasks": "Alitehtävät",
"moveToTop": "Siirrä ylimmäksi",
"deleteTask": "Poista tehtävä",
"newSubtask": "Uusi alitehtävä",
- "deleteTaskConfirmation": "Poistetaanko \"{title}\" Google Tasksista?",
+ "deleteTaskConfirmation": "Poistetaanko \"{title}\"?",
"metadata": "Metatiedot",
"id": "Tunnus",
"etag": "ETag",
@@ -370,5 +371,201 @@
"selectTimeZone": "Valitse aikavyöhyke",
"searchLocations": "Hae sijainteja",
"noLocationsFound": "Sijainteja ei löytynyt",
- "deleteCalendarConfirmation": "Poistetaanko \"{title}\"?"
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
+ "deleteCalendarConfirmation": "Poistetaanko \"{title}\"?",
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {
+ "placeholders": {
+ "host": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {
+ "placeholders": {
+ "code": {
+ "type": "String"
+ }
+ }
+ },
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "Määräaika ei saa olla ennen alkamisaikaa.",
+ "taskStartDueTimeModeMismatch": "Aseta kellonaika sekä alkamiselle että määräajalle tai tee tehtävästä koko päivän tehtävä.",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "unshare": "Lopeta jakaminen",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "Vaatii toimenpiteitä",
+ "taskStatusInProcess": "Käsittelyssä",
+ "taskStatusCompleted": "Valmiina",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "completionDate": "Completion date",
+ "priority": "Tärkeys",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "priorityLowValue": "Priority {priority} · Low",
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
+ "reminders": "Reminders",
+ "noReminders": "No reminders",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "Before the task starts",
+ "beforeTaskDue": "Before the task is due",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "discardRelatedReminders": "Discard reminders",
+ "keepRelatedReminders": "Keep reminders",
+ "repeatEvery": "Toista joka",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "Lopeta toisto",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "repeatEveryWeeks": "Every {count} weeks",
+ "repeatEveryMonths": "Every {count} months",
+ "repeatEveryYears": "Every {count} years",
+ "repeatOnDaysSummary": "on {days}",
+ "repeatOnMonthDaysSummary": "päivänä {days}",
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "repeatInMonthsSummary": "in {months}",
+ "repeatTimesSummary": "{count} kertaa",
+ "repeatUntilSummary": "{date} asti",
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "duplicateTask": "Duplicate task",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "hideSubtasks": "Piilota alitehtävät",
+ "hideClosedSubtasks": "Piilota suljetut alitehtävät",
+ "reminderUnit": "Unit"
}
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
index 7d4d9a8..b5a5780 100644
--- a/lib/l10n/app_fr.arb
+++ b/lib/l10n/app_fr.arb
@@ -1,14 +1,14 @@
{
"@@locale": "fr",
"appTitle": "BusyMax",
- "connectGoogleAccount": "Connectez des comptes Google et Microsoft pour synchroniser calendriers et tâches.",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "Sur l’écran d’autorisations Google, sélectionnez les autorisations Calendrier et Tâches.",
"googlePermissionsRequiredRetry": "Les autorisations Google Calendar et Google Tasks sont requises. Réessayez et sélectionnez les deux cases.",
"finishSetup": "Terminer la configuration",
"continueSetup": "Continuer",
"onboardingSetupTitle": "Configurer BusyMax",
"onboardingAccountsStepTitle": "Connecter des comptes",
- "onboardingAccountsStepDescription": "Ajoutez tous les comptes Google et Microsoft que vous voulez utiliser. BusyMax synchronise les calendriers, événements, listes de tâches et tâches de chaque compte.",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "Choisir les paramètres système",
"onboardingPreferencesStepDescription": "Réglez le comportement de l’application sur le bureau, les rappels, le niveau de détail des notifications et l’apparence avant d’ouvrir votre planning.",
"signInWithGoogle": "Se connecter avec Google",
@@ -94,12 +94,30 @@
"formatUnderlineShortLabel": "S",
"formatUnderlineTooltip": "Souligné",
"reminderMinutesBefore": "{minutes, plural, =1{1 minute avant} other{{minutes} minutes avant}}",
- "@reminderMinutesBefore": {"placeholders": {"minutes": {"type": "int"}}},
+ "@reminderMinutesBefore": {
+ "placeholders": {
+ "minutes": {
+ "type": "int"
+ }
+ }
+ },
"reminderAtStart": "À l’heure de début",
"reminderHoursBefore": "{hours, plural, =1{1 heure avant} other{{hours} heures avant}}",
- "@reminderHoursBefore": {"placeholders": {"hours": {"type": "int"}}},
+ "@reminderHoursBefore": {
+ "placeholders": {
+ "hours": {
+ "type": "int"
+ }
+ }
+ },
"reminderDaysBefore": "{days, plural, =1{1 jour avant} other{{days} jours avant}}",
- "@reminderDaysBefore": {"placeholders": {"days": {"type": "int"}}},
+ "@reminderDaysBefore": {
+ "placeholders": {
+ "days": {
+ "type": "int"
+ }
+ }
+ },
"availabilityFree": "Disponible",
"availabilityTentative": "Provisoire",
"availabilityOutOfOffice": "Absent du bureau",
@@ -183,14 +201,20 @@
"removingAccount": "Suppression du compte…",
"removeAccountDescription": "Arrêter la synchronisation et supprimer les données de ce compte de cet appareil.",
"removeAccountTitle": "Supprimer {account} de BusyMax ?",
- "@removeAccountTitle": {"placeholders": {"account": {"type": "String"}}},
- "removeAccountConfirmation": "Cette action supprime de cet appareil les tâches, calendriers, événements, rappels et modifications hors ligne en attente mis en cache. Les modifications non synchronisées seront perdues. Aucune donnée ne sera supprimée de Google ou Microsoft.",
+ "@removeAccountTitle": {
+ "placeholders": {
+ "account": {
+ "type": "String"
+ }
+ }
+ },
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "Révoquer également l’accès de BusyMax à ce compte Google",
"revokeGoogleAccessDescription": "Vous devrez accorder à nouveau l’accès avant de reconnecter le compte.",
"removeAccountAction": "Supprimer le compte",
"removeAccountFailed": "Impossible de terminer la suppression du compte. Réessayez.",
"accountRemovedGoogleRevokeFailed": "Le compte a été supprimé de cet appareil, mais BusyMax n’a pas pu révoquer son accès à votre compte Google. Vous pouvez révoquer cet accès depuis votre compte Google.",
- "newList": "Nouvelle liste",
+ "newTaskList": "Nouvelle liste de tâches",
"signInToViewTaskLists": "Connectez-vous pour voir les listes de tâches.",
"noTaskListsSynced": "Aucune liste de tâches synchronisée.",
"listActions": "Actions de liste",
@@ -211,9 +235,21 @@
"listRefreshed": "Liste actualisée.",
"allTasksRefreshed": "Tous les comptes ont été actualisés.",
"exportedFile": "Exporté vers {path}",
- "@exportedFile": {"placeholders": {"path": {"type": "String"}}},
+ "@exportedFile": {
+ "placeholders": {
+ "path": {
+ "type": "String"
+ }
+ }
+ },
"exportFailed": "Échec de l’export : {error}",
- "@exportFailed": {"placeholders": {"error": {"type": "String"}}},
+ "@exportFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
"refreshFailed": "Échec de l’actualisation : {error}",
"selectOrCreateTaskList": "Sélectionnez ou créez une liste de tâches pour commencer.",
"signInToViewTasks": "Connectez-vous pour voir les tâches.",
@@ -279,10 +315,11 @@
"list": "Liste",
"microsoftMoveUnsupported": "Le déplacement entre listes n’est pas pris en charge pour les comptes Microsoft To Do dans cette version.",
"createSubtask": "Créer une sous-tâche",
+ "subtasks": "Sous-tâches",
"moveToTop": "Déplacer tout en haut",
"deleteTask": "Supprimer la tâche",
"newSubtask": "Nouvelle sous-tâche",
- "deleteTaskConfirmation": "Supprimer « {title} » de Google Tasks ?",
+ "deleteTaskConfirmation": "Supprimer « {title} » ?",
"metadata": "Métadonnées",
"id": "ID",
"etag": "ETag",
@@ -370,14 +407,228 @@
"nextYear": "Année suivante",
"openYearView": "Ouvrir la vue annuelle",
"weekNumberTooltip": "Semaine {number}",
- "@weekNumberTooltip": {"placeholders": {"number": {"type": "int"}}},
+ "@weekNumberTooltip": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ }
+ }
+ },
"resizeAllDayPanel": "Redimensionner le volet des événements sur toute la journée",
"scheduleItemCount": "{count, plural, =1{1 élément} other{{count} éléments}}",
- "@scheduleItemCount": {"placeholders": {"count": {"type": "int"}}},
+ "@scheduleItemCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
"readOnlyCalendar": "Ce calendrier est en lecture seule.",
"selectTimeZone": "Sélectionner le fuseau horaire",
"searchLocations": "Rechercher des lieux",
"noLocationsFound": "Aucun lieu trouvé",
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
"deleteCalendarConfirmation": "Supprimer « {title} » ?",
- "@deleteCalendarConfirmation": {"placeholders": {"title": {"type": "String"}}}
+ "@deleteCalendarConfirmation": {
+ "placeholders": {
+ "title": {
+ "type": "String"
+ }
+ }
+ },
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {
+ "placeholders": {
+ "host": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {
+ "placeholders": {
+ "code": {
+ "type": "String"
+ }
+ }
+ },
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "L’échéance ne peut pas précéder le début.",
+ "taskStartDueTimeModeMismatch": "Définissez une heure pour le début et l’échéance, ou passez la tâche en journée entière.",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "unshare": "Ne plus partager",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "Nécessite une action",
+ "taskStatusInProcess": "En cours",
+ "taskStatusCompleted": "Terminé",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "completionDate": "Completion date",
+ "priority": "Priorité",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "priorityLowValue": "Priority {priority} · Low",
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
+ "reminders": "Reminders",
+ "noReminders": "Aucun rappel",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "Avant le début de la tâche",
+ "beforeTaskDue": "Avant l'échéance de la tâche",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "discardRelatedReminders": "Discard reminders",
+ "keepRelatedReminders": "Keep reminders",
+ "repeatEvery": "Répéter chaque",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "Arrêter la répétition",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "repeatEveryWeeks": "Every {count} weeks",
+ "repeatEveryMonths": "Every {count} months",
+ "repeatEveryYears": "Every {count} years",
+ "repeatOnDaysSummary": "on {days}",
+ "repeatOnMonthDaysSummary": "le {days}",
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "repeatInMonthsSummary": "in {months}",
+ "repeatTimesSummary": "{count} fois",
+ "repeatUntilSummary": "jusqu'au {date}",
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "duplicateTask": "Duplicate task",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "hideSubtasks": "Masquer les sous-tâches",
+ "hideClosedSubtasks": "Masquer les sous-tâches fermées",
+ "reminderUnit": "Unit"
}
diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb
index a809900..a041bb1 100644
--- a/lib/l10n/app_hi.arb
+++ b/lib/l10n/app_hi.arb
@@ -1,14 +1,14 @@
{
"@@locale": "hi",
"appTitle": "BusyMax",
- "connectGoogleAccount": "कैलेंडर और कार्य सिंक करने के लिए Google और Microsoft खाते कनेक्ट करें।",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "Google अनुमति स्क्रीन पर, कैलेंडर और कार्य दोनों अनुमतियाँ चुनें।",
"googlePermissionsRequiredRetry": "Google Calendar और Google Tasks की अनुमतियाँ आवश्यक हैं। फिर से कोशिश करें और दोनों चेकबॉक्स चुनें।",
"finishSetup": "सेटअप पूरा करें",
"continueSetup": "जारी रखें",
"onboardingSetupTitle": "BusyMax सेट अप करें",
"onboardingAccountsStepTitle": "खाते कनेक्ट करें",
- "onboardingAccountsStepDescription": "वे सभी Google और Microsoft खाते जोड़ें जिन्हें आप उपयोग करना चाहते हैं। BusyMax प्रत्येक खाते के कैलेंडर, ईवेंट, कार्य सूचियाँ और कार्य सिंक करता है।",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "सिस्टम सेटिंग्स चुनें",
"onboardingPreferencesStepDescription": "अपना शेड्यूल खोलने से पहले डेस्कटॉप व्यवहार, रिमाइंडर, सूचनाओं के विवरण का स्तर और दिखावट सेट करें।",
"signInWithGoogle": "Google से साइन इन करें",
@@ -180,13 +180,13 @@
"removingAccount": "खाता हटाया जा रहा है…",
"removeAccountDescription": "सिंक करना बंद करें और इस डिवाइस से इस खाते का डेटा हटाएँ।",
"removeAccountTitle": "BusyMax से {account} हटाएँ?",
- "removeAccountConfirmation": "इससे कैश किए गए कार्य, कैलेंडर, ईवेंट, रिमाइंडर और लंबित ऑफ़लाइन बदलाव इस डिवाइस से मिट जाएँगे। सिंक न किए गए बदलाव खो जाएँगे। Google या Microsoft से कुछ भी नहीं मिटेगा।",
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "इस Google खाते से BusyMax की पहुँच भी रद्द करें",
"revokeGoogleAccessDescription": "दोबारा कनेक्ट करने से पहले आपको फिर से पहुँच देनी होगी।",
"removeAccountAction": "खाता हटाएँ",
"removeAccountFailed": "खाता हटाना पूरा नहीं हो सका। फिर से कोशिश करें।",
"accountRemovedGoogleRevokeFailed": "खाता इस डिवाइस से हटा दिया गया, लेकिन BusyMax की Google खाते तक पहुँच रद्द नहीं की जा सकी। आप यह पहुँच अपने Google खाते से रद्द कर सकते हैं।",
- "newList": "नई सूची",
+ "newTaskList": "नई कार्य सूची",
"signInToViewTaskLists": "कार्य सूचियाँ देखने के लिए साइन इन करें।",
"noTaskListsSynced": "अभी तक कोई कार्य सूची सिंक नहीं हुई है।",
"listActions": "सूची की कार्रवाइयाँ",
@@ -273,10 +273,11 @@
"list": "सूची",
"microsoftMoveUnsupported": "इस संस्करण में Microsoft To Do खातों के लिए सूचियों के बीच कार्य ले जाना समर्थित नहीं है।",
"createSubtask": "उपकार्य बनाएँ",
+ "subtasks": "उपकार्य",
"moveToTop": "सबसे ऊपर ले जाएँ",
"deleteTask": "कार्य मिटाएँ",
"newSubtask": "नया उपकार्य",
- "deleteTaskConfirmation": "Google Tasks से “{title}” मिटाएँ?",
+ "deleteTaskConfirmation": "“{title}” मिटाएँ?",
"metadata": "मेटाडेटा",
"id": "आईडी",
"etag": "ETag",
@@ -370,5 +371,201 @@
"selectTimeZone": "समय क्षेत्र चुनें",
"searchLocations": "स्थान खोजें",
"noLocationsFound": "कोई स्थान नहीं मिला",
- "deleteCalendarConfirmation": "“{title}” मिटाएँ?"
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
+ "deleteCalendarConfirmation": "“{title}” मिटाएँ?",
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {
+ "placeholders": {
+ "host": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {
+ "placeholders": {
+ "code": {
+ "type": "String"
+ }
+ }
+ },
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "नियत समय प्रारंभ समय से पहले नहीं हो सकता।",
+ "taskStartDueTimeModeMismatch": "प्रारंभ और नियत समय दोनों सेट करें, या कार्य को पूरे दिन का बनाएँ।",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "unshare": "Unshare",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "Needs action",
+ "taskStatusInProcess": "In process",
+ "taskStatusCompleted": "Completed",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "completionDate": "Completion date",
+ "priority": "Priority",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "priorityLowValue": "Priority {priority} · Low",
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
+ "reminders": "Reminders",
+ "noReminders": "No reminders",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "Before the task starts",
+ "beforeTaskDue": "Before the task is due",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "discardRelatedReminders": "Discard reminders",
+ "keepRelatedReminders": "Keep reminders",
+ "repeatEvery": "Repeat every",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "End repeat",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "repeatEveryWeeks": "Every {count} weeks",
+ "repeatEveryMonths": "Every {count} months",
+ "repeatEveryYears": "Every {count} years",
+ "repeatOnDaysSummary": "on {days}",
+ "repeatOnMonthDaysSummary": "on day {days}",
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "repeatInMonthsSummary": "in {months}",
+ "repeatTimesSummary": "{count} times",
+ "repeatUntilSummary": "until {date}",
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "duplicateTask": "Duplicate task",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "hideSubtasks": "Hide subtasks",
+ "hideClosedSubtasks": "Hide closed subtasks",
+ "reminderUnit": "Unit"
}
diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb
index f486c22..9193832 100644
--- a/lib/l10n/app_it.arb
+++ b/lib/l10n/app_it.arb
@@ -1,14 +1,14 @@
{
"@@locale": "it",
"appTitle": "BusyMax",
- "connectGoogleAccount": "Collega gli account Google e Microsoft per sincronizzare calendari e attività.",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "Nella schermata delle autorizzazioni di Google, seleziona sia l’autorizzazione per il calendario sia quella per le attività.",
"googlePermissionsRequiredRetry": "Sono necessarie le autorizzazioni per Google Calendar e Google Tasks. Riprova e seleziona entrambe le caselle.",
"finishSetup": "Completa la configurazione",
"continueSetup": "Continua",
"onboardingSetupTitle": "Configura BusyMax",
"onboardingAccountsStepTitle": "Collega gli account",
- "onboardingAccountsStepDescription": "Aggiungi tutti gli account Google e Microsoft che vuoi utilizzare. BusyMax sincronizza calendari, eventi, elenchi di attività e attività di ogni account.",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "Scegli le impostazioni di sistema",
"onboardingPreferencesStepDescription": "Prima di aprire l’agenda, configura il comportamento sul desktop, i promemoria, il livello di dettaglio delle notifiche e l’aspetto.",
"signInWithGoogle": "Accedi con Google",
@@ -180,13 +180,13 @@
"removingAccount": "Rimozione account…",
"removeAccountDescription": "Interrompi la sincronizzazione e rimuovi i dati di questo account dal dispositivo.",
"removeAccountTitle": "Rimuovere {account} da BusyMax?",
- "removeAccountConfirmation": "Questa azione elimina dal dispositivo attività, calendari, eventi e promemoria memorizzati nella cache, oltre alle modifiche offline in sospeso. Le modifiche non sincronizzate andranno perse. Non verrà eliminato nulla da Google o Microsoft.",
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "Revoca anche l’accesso di BusyMax a questo account Google",
"revokeGoogleAccessDescription": "Dovrai concedere nuovamente l’accesso prima di riconnetterti.",
"removeAccountAction": "Rimuovi account",
"removeAccountFailed": "Impossibile completare la rimozione dell’account. Riprova.",
"accountRemovedGoogleRevokeFailed": "L’account è stato rimosso da questo dispositivo, ma BusyMax non è riuscito a revocare il proprio accesso a Google. Puoi revocarlo dal tuo account Google.",
- "newList": "Nuovo elenco",
+ "newTaskList": "Nuovo elenco di attività",
"signInToViewTaskLists": "Accedi per visualizzare gli elenchi di attività.",
"noTaskListsSynced": "Nessun elenco di attività ancora sincronizzato.",
"listActions": "Azioni dell’elenco",
@@ -273,10 +273,11 @@
"list": "Elenco",
"microsoftMoveUnsupported": "In questa versione non è possibile spostare attività tra elenchi negli account Microsoft To Do.",
"createSubtask": "Crea sottoattività",
+ "subtasks": "Sottoattività",
"moveToTop": "Sposta in cima",
"deleteTask": "Elimina attività",
"newSubtask": "Nuova sottoattività",
- "deleteTaskConfirmation": "Eliminare «{title}» da Google Tasks?",
+ "deleteTaskConfirmation": "Eliminare «{title}»?",
"metadata": "Metadati",
"id": "ID",
"etag": "ETag",
@@ -370,5 +371,201 @@
"selectTimeZone": "Seleziona fuso orario",
"searchLocations": "Cerca luoghi",
"noLocationsFound": "Nessun luogo trovato",
- "deleteCalendarConfirmation": "Eliminare «{title}»?"
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
+ "deleteCalendarConfirmation": "Eliminare «{title}»?",
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {
+ "placeholders": {
+ "host": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {
+ "placeholders": {
+ "code": {
+ "type": "String"
+ }
+ }
+ },
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "La scadenza non può precedere l'inizio.",
+ "taskStartDueTimeModeMismatch": "Imposta un orario sia per l'inizio sia per la scadenza, oppure rendi l'attività valida per l'intera giornata.",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "unshare": "Rimuovi condivisione",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "Richiede azione",
+ "taskStatusInProcess": "In corso",
+ "taskStatusCompleted": "Completato",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "completionDate": "Completion date",
+ "priority": "Priorità",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "priorityLowValue": "Priority {priority} · Low",
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
+ "reminders": "Reminders",
+ "noReminders": "Nessun promemoria",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "Prima dell'inizio dell'attività",
+ "beforeTaskDue": "Prima della scadenza del compito",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "discardRelatedReminders": "Scarta i promemoria",
+ "keepRelatedReminders": "Mantieni i promemoria",
+ "repeatEvery": "Ripeti ogni",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "Termina ripetizione",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "repeatEveryWeeks": "Every {count} weeks",
+ "repeatEveryMonths": "Every {count} months",
+ "repeatEveryYears": "Every {count} years",
+ "repeatOnDaysSummary": "on {days}",
+ "repeatOnMonthDaysSummary": "il giorno {days}",
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "repeatInMonthsSummary": "in {months}",
+ "repeatTimesSummary": "{count} volte",
+ "repeatUntilSummary": "fino al {date}",
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "duplicateTask": "Duplicate task",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "hideSubtasks": "Nascondi sottoattività",
+ "hideClosedSubtasks": "Nascondi sotto-attività chiuse",
+ "reminderUnit": "Unit"
}
diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb
index 06b002e..9908fe8 100644
--- a/lib/l10n/app_ja.arb
+++ b/lib/l10n/app_ja.arb
@@ -1,14 +1,14 @@
{
"@@locale": "ja",
"appTitle": "BusyMax",
- "connectGoogleAccount": "Google と Microsoft のアカウントを接続して、カレンダーとタスクを同期します。",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "Google の権限画面で、カレンダーとタスクの両方の権限を選択してください。",
"googlePermissionsRequiredRetry": "Google カレンダーと Google Tasks の権限が必要です。もう一度試して、両方のチェックボックスを選択してください。",
"finishSetup": "セットアップを完了",
"continueSetup": "続行",
"onboardingSetupTitle": "BusyMax をセットアップ",
"onboardingAccountsStepTitle": "アカウントを接続",
- "onboardingAccountsStepDescription": "使用するすべての Google アカウントと Microsoft アカウントを追加してください。BusyMax は各アカウントのカレンダー、予定、タスクリスト、タスクを同期します。",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "システム設定を選択",
"onboardingPreferencesStepDescription": "スケジュールを開く前に、デスクトップでの動作、リマインダー、通知の詳細度、外観を設定します。",
"signInWithGoogle": "Google でサインイン",
@@ -180,13 +180,13 @@
"removingAccount": "アカウントを削除しています…",
"removeAccountDescription": "同期を停止し、このアカウントのデータをこのデバイスから削除します。",
"removeAccountTitle": "BusyMax から {account} を削除しますか?",
- "removeAccountConfirmation": "このデバイスにキャッシュされたタスク、カレンダー、予定、リマインダー、保留中のオフライン変更が削除されます。同期されていない変更は失われます。Google または Microsoft から削除されるデータはありません。",
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "この Google アカウントへの BusyMax のアクセス権も取り消す",
"revokeGoogleAccessDescription": "再接続する前に、もう一度アクセスを許可する必要があります。",
"removeAccountAction": "アカウントを削除",
"removeAccountFailed": "アカウントの削除を完了できませんでした。もう一度お試しください。",
"accountRemovedGoogleRevokeFailed": "アカウントはこのデバイスから削除されましたが、BusyMax は Google アカウントへのアクセス権を取り消せませんでした。Google アカウントの設定から取り消すことができます。",
- "newList": "新しいリスト",
+ "newTaskList": "新しいタスクリスト",
"signInToViewTaskLists": "タスクリストを表示するにはサインインしてください。",
"noTaskListsSynced": "同期済みのタスクリストはまだありません。",
"listActions": "リストの操作",
@@ -273,10 +273,11 @@
"list": "リスト",
"microsoftMoveUnsupported": "このバージョンでは、Microsoft To Do アカウントのリスト間でタスクを移動できません。",
"createSubtask": "サブタスクを作成",
+ "subtasks": "サブタスク",
"moveToTop": "一番上に移動",
"deleteTask": "タスクを削除",
"newSubtask": "新しいサブタスク",
- "deleteTaskConfirmation": "Google Tasks から「{title}」を削除しますか?",
+ "deleteTaskConfirmation": "「{title}」を削除しますか?",
"metadata": "メタデータ",
"id": "ID",
"etag": "ETag",
@@ -370,5 +371,201 @@
"selectTimeZone": "タイムゾーンを選択",
"searchLocations": "場所を検索",
"noLocationsFound": "場所が見つかりません",
- "deleteCalendarConfirmation": "「{title}」を削除しますか?"
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
+ "deleteCalendarConfirmation": "「{title}」を削除しますか?",
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {
+ "placeholders": {
+ "host": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {
+ "placeholders": {
+ "code": {
+ "type": "String"
+ }
+ }
+ },
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "期限を開始より前に設定することはできません。",
+ "taskStartDueTimeModeMismatch": "開始と期限の両方に時刻を設定するか、タスクを終日にしてください。",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "unshare": "共有を解除",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "アクションが必要",
+ "taskStatusInProcess": "進行中",
+ "taskStatusCompleted": "完了",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "completionDate": "Completion date",
+ "priority": "優先度",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "priorityLowValue": "Priority {priority} · Low",
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
+ "reminders": "Reminders",
+ "noReminders": "No reminders",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "Before the task starts",
+ "beforeTaskDue": "Before the task is due",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "discardRelatedReminders": "Discard reminders",
+ "keepRelatedReminders": "Keep reminders",
+ "repeatEvery": "毎日繰り返す",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "繰り返し終了",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "repeatEveryWeeks": "Every {count} weeks",
+ "repeatEveryMonths": "Every {count} months",
+ "repeatEveryYears": "Every {count} years",
+ "repeatOnDaysSummary": "on {days}",
+ "repeatOnMonthDaysSummary": "on day {days}",
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "repeatInMonthsSummary": "in {months}",
+ "repeatTimesSummary": "{count}回",
+ "repeatUntilSummary": "{date}まで",
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "duplicateTask": "Duplicate task",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "hideSubtasks": "サブタスクを非表示",
+ "hideClosedSubtasks": "Hide closed subtasks",
+ "reminderUnit": "Unit"
}
diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb
index 183daaf..b41811c 100644
--- a/lib/l10n/app_ko.arb
+++ b/lib/l10n/app_ko.arb
@@ -1,14 +1,14 @@
{
"@@locale": "ko",
"appTitle": "BusyMax",
- "connectGoogleAccount": "Google 및 Microsoft 계정을 연결하여 캘린더와 할 일을 동기화하세요.",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "Google 권한 화면에서 캘린더와 할 일 권한을 모두 선택하세요.",
"googlePermissionsRequiredRetry": "Google Calendar 및 Google Tasks 권한이 필요합니다. 다시 시도하여 두 체크박스를 모두 선택하세요.",
"finishSetup": "설정 완료",
"continueSetup": "계속",
"onboardingSetupTitle": "BusyMax 설정",
"onboardingAccountsStepTitle": "계정 연결",
- "onboardingAccountsStepDescription": "사용할 Google 및 Microsoft 계정을 모두 추가하세요. BusyMax는 각 계정의 캘린더, 일정, 할 일 목록 및 할 일을 동기화합니다.",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "시스템 설정 선택",
"onboardingPreferencesStepDescription": "일정을 열기 전에 데스크톱 동작, 미리 알림, 알림 세부 수준 및 화면 모양을 설정하세요.",
"signInWithGoogle": "Google로 로그인",
@@ -180,13 +180,13 @@
"removingAccount": "계정 삭제 중…",
"removeAccountDescription": "동기화를 중지하고 이 기기에서 이 계정의 데이터를 삭제합니다.",
"removeAccountTitle": "BusyMax에서 {account} 계정을 삭제할까요?",
- "removeAccountConfirmation": "이 기기에서 캐시된 할 일, 캘린더, 일정, 미리 알림 및 보류 중인 오프라인 변경 사항이 삭제됩니다. 동기화되지 않은 변경 사항은 사라집니다. Google 또는 Microsoft에서는 아무것도 삭제되지 않습니다.",
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "이 Google 계정에 대한 BusyMax의 액세스 권한도 취소",
"revokeGoogleAccessDescription": "다시 연결하기 전에 액세스 권한을 다시 부여해야 합니다.",
"removeAccountAction": "계정 삭제",
"removeAccountFailed": "계정 삭제를 완료할 수 없습니다. 다시 시도하세요.",
"accountRemovedGoogleRevokeFailed": "계정은 이 기기에서 삭제되었지만 BusyMax가 Google 계정 액세스 권한을 취소하지 못했습니다. Google 계정에서 직접 취소할 수 있습니다.",
- "newList": "새 목록",
+ "newTaskList": "새 할 일 목록",
"signInToViewTaskLists": "할 일 목록을 보려면 로그인하세요.",
"noTaskListsSynced": "아직 동기화된 할 일 목록이 없습니다.",
"listActions": "목록 작업",
@@ -273,10 +273,11 @@
"list": "목록",
"microsoftMoveUnsupported": "이 버전에서는 Microsoft To Do 계정의 목록 간에 할 일을 이동할 수 없습니다.",
"createSubtask": "하위 할 일 만들기",
+ "subtasks": "하위 할 일",
"moveToTop": "맨 위로 이동",
"deleteTask": "할 일 삭제",
"newSubtask": "새 하위 할 일",
- "deleteTaskConfirmation": "Google Tasks에서 “{title}” 항목을 삭제할까요?",
+ "deleteTaskConfirmation": "“{title}” 항목을 삭제할까요?",
"metadata": "메타데이터",
"id": "ID",
"etag": "ETag",
@@ -370,5 +371,201 @@
"selectTimeZone": "시간대 선택",
"searchLocations": "위치 검색",
"noLocationsFound": "위치를 찾을 수 없습니다",
- "deleteCalendarConfirmation": "“{title}” 캘린더를 삭제할까요?"
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
+ "deleteCalendarConfirmation": "“{title}” 캘린더를 삭제할까요?",
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {
+ "placeholders": {
+ "host": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {
+ "placeholders": {
+ "code": {
+ "type": "String"
+ }
+ }
+ },
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "마감은 시작보다 빠를 수 없습니다.",
+ "taskStartDueTimeModeMismatch": "시작과 마감에 모두 시간을 설정하거나 작업을 종일로 설정하세요.",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "unshare": "공유 해제",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "Needs action",
+ "taskStatusInProcess": "In process",
+ "taskStatusCompleted": "완료됨",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "completionDate": "Completion date",
+ "priority": "우선 순위",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "priorityLowValue": "Priority {priority} · Low",
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
+ "reminders": "Reminders",
+ "noReminders": "No reminders",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "Before the task starts",
+ "beforeTaskDue": "Before the task is due",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "discardRelatedReminders": "Discard reminders",
+ "keepRelatedReminders": "Keep reminders",
+ "repeatEvery": "반복 주기",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "반복 종료",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "repeatEveryWeeks": "Every {count} weeks",
+ "repeatEveryMonths": "Every {count} months",
+ "repeatEveryYears": "Every {count} years",
+ "repeatOnDaysSummary": "on {days}",
+ "repeatOnMonthDaysSummary": "on day {days}",
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "repeatInMonthsSummary": "in {months}",
+ "repeatTimesSummary": "{count}회",
+ "repeatUntilSummary": "until {date}",
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "duplicateTask": "Duplicate task",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "hideSubtasks": "Hide subtasks",
+ "hideClosedSubtasks": "Hide closed subtasks",
+ "reminderUnit": "Unit"
}
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index d330459..0626073 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -1,14 +1,14 @@
{
"@@locale": "pt",
"appTitle": "BusyMax",
- "connectGoogleAccount": "Ligue contas Google e Microsoft para sincronizar calendários e tarefas.",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "No ecrã de autorizações da Google, selecione as autorizações do Calendário e das Tarefas.",
"googlePermissionsRequiredRetry": "As autorizações do Calendário Google e do Google Tasks são necessárias. Tente novamente e selecione ambas as caixas.",
"finishSetup": "Concluir configuração",
"continueSetup": "Continuar",
"onboardingSetupTitle": "Configurar o BusyMax",
"onboardingAccountsStepTitle": "Ligar contas",
- "onboardingAccountsStepDescription": "Adicione todas as contas Google e Microsoft que pretende utilizar. O BusyMax sincroniza calendários, eventos, listas de tarefas e tarefas de cada conta.",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "Escolher definições do sistema",
"onboardingPreferencesStepDescription": "Configure o comportamento da aplicação no ambiente de trabalho, os lembretes, o nível de detalhe das notificações e o aspeto antes de abrir a agenda.",
"signInWithGoogle": "Iniciar sessão com a Google",
@@ -180,13 +180,13 @@
"removingAccount": "A remover conta…",
"removeAccountDescription": "Parar a sincronização e remover os dados desta conta deste dispositivo.",
"removeAccountTitle": "Remover {account} do BusyMax?",
- "removeAccountConfirmation": "Esta ação elimina deste dispositivo as tarefas, os calendários, os eventos, os lembretes e as alterações offline pendentes em cache. As alterações não sincronizadas serão perdidas. Nada será eliminado da Google ou da Microsoft.",
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "Revogar também o acesso do BusyMax a esta conta Google",
"revokeGoogleAccessDescription": "Terá de conceder acesso novamente antes de voltar a ligar a conta.",
"removeAccountAction": "Remover conta",
"removeAccountFailed": "Não foi possível concluir a remoção da conta. Tente novamente.",
"accountRemovedGoogleRevokeFailed": "A conta foi removida deste dispositivo, mas não foi possível revogar o acesso do BusyMax à sua conta Google. Pode revogar esse acesso na sua conta Google.",
- "newList": "Nova lista",
+ "newTaskList": "Nova lista de tarefas",
"signInToViewTaskLists": "Inicie sessão para ver as listas de tarefas.",
"noTaskListsSynced": "Ainda não há listas de tarefas sincronizadas.",
"listActions": "Ações da lista",
@@ -273,10 +273,11 @@
"list": "Lista",
"microsoftMoveUnsupported": "Nesta versão, não é possível mover tarefas entre listas em contas Microsoft To Do.",
"createSubtask": "Criar subtarefa",
+ "subtasks": "Subtarefas",
"moveToTop": "Mover para o início",
"deleteTask": "Eliminar tarefa",
"newSubtask": "Nova subtarefa",
- "deleteTaskConfirmation": "Eliminar «{title}» do Google Tasks?",
+ "deleteTaskConfirmation": "Eliminar «{title}»?",
"metadata": "Metadados",
"id": "ID",
"etag": "ETag",
@@ -370,5 +371,201 @@
"selectTimeZone": "Selecionar fuso horário",
"searchLocations": "Pesquisar locais",
"noLocationsFound": "Nenhum local encontrado",
- "deleteCalendarConfirmation": "Eliminar «{title}»?"
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
+ "deleteCalendarConfirmation": "Eliminar «{title}»?",
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {
+ "placeholders": {
+ "host": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {
+ "placeholders": {
+ "code": {
+ "type": "String"
+ }
+ }
+ },
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "O prazo não pode ser anterior ao início.",
+ "taskStartDueTimeModeMismatch": "Defina horários para o início e o prazo, ou torne a tarefa de dia inteiro.",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "unshare": "Cancelar partilha",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "Needs action",
+ "taskStatusInProcess": "Em andamento",
+ "taskStatusCompleted": "Concluída",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "completionDate": "Completion date",
+ "priority": "Prioridade",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "priorityLowValue": "Priority {priority} · Low",
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
+ "reminders": "Reminders",
+ "noReminders": "No reminders",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "Before the task starts",
+ "beforeTaskDue": "Before the task is due",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "discardRelatedReminders": "Discard reminders",
+ "keepRelatedReminders": "Keep reminders",
+ "repeatEvery": "Repetir a cada",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "End repeat",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "repeatEveryWeeks": "Every {count} weeks",
+ "repeatEveryMonths": "Every {count} months",
+ "repeatEveryYears": "Every {count} years",
+ "repeatOnDaysSummary": "em {days}",
+ "repeatOnMonthDaysSummary": "on day {days}",
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "repeatInMonthsSummary": "in {months}",
+ "repeatTimesSummary": "{count} vezes",
+ "repeatUntilSummary": "até {date}",
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "duplicateTask": "Duplicate task",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "hideSubtasks": "Ocultar subtarefas",
+ "hideClosedSubtasks": "Hide closed subtasks",
+ "reminderUnit": "Unit"
}
diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb
index 6184aa3..ae3c1ab 100644
--- a/lib/l10n/app_ru.arb
+++ b/lib/l10n/app_ru.arb
@@ -1,14 +1,14 @@
{
"@@locale": "ru",
"appTitle": "BusyMax",
- "connectGoogleAccount": "Подключите аккаунты Google и Microsoft, чтобы синхронизировать календари и задачи.",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "На экране запроса доступа Google установите флажки «Google Календарь» и «Google Задачи».",
"googlePermissionsRequiredRetry": "BusyMax требуется доступ к Google Календарю и Google Задачам. Повторите попытку и установите оба флажка.",
"finishSetup": "Завершить настройку",
"continueSetup": "Продолжить",
"onboardingSetupTitle": "Настройка BusyMax",
"onboardingAccountsStepTitle": "Подключите аккаунты",
- "onboardingAccountsStepDescription": "Добавьте все аккаунты Google и Microsoft, которые хотите использовать. BusyMax синхронизирует календари, события, списки задач и задачи из каждого аккаунта.",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "Выберите системные параметры",
"onboardingPreferencesStepDescription": "Настройте поведение приложения на рабочем столе, напоминания, уровень детализации уведомлений и внешний вид, прежде чем открыть расписание.",
"signInWithGoogle": "Войти через Google",
@@ -180,13 +180,13 @@
"removingAccount": "Удаление аккаунта…",
"removeAccountDescription": "Остановить синхронизацию и удалить данные этого аккаунта с устройства.",
"removeAccountTitle": "Удалить {account} из BusyMax?",
- "removeAccountConfirmation": "С этого устройства будут удалены кэшированные задачи, календари, события, напоминания и локальные изменения, ожидающие синхронизации. Несинхронизированные изменения будут потеряны. В Google и Microsoft ничего не будет удалено.",
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "Также отозвать у BusyMax доступ к этому аккаунту Google",
"revokeGoogleAccessDescription": "Перед повторным подключением аккаунта потребуется снова предоставить доступ.",
"removeAccountAction": "Удалить аккаунт",
"removeAccountFailed": "Не удалось завершить удаление аккаунта. Повторите попытку.",
"accountRemovedGoogleRevokeFailed": "Аккаунт удалён с этого устройства, но отозвать доступ BusyMax к Google не удалось. Вы можете отозвать доступ в аккаунте Google.",
- "newList": "Новый список",
+ "newTaskList": "Новый список задач",
"signInToViewTaskLists": "Войдите, чтобы просмотреть списки задач.",
"noTaskListsSynced": "Синхронизированных списков задач пока нет.",
"listActions": "Действия со списком",
@@ -273,10 +273,11 @@
"list": "Список",
"microsoftMoveUnsupported": "В этой версии перенос между списками для аккаунтов Microsoft To Do не поддерживается.",
"createSubtask": "Создать подзадачу",
+ "subtasks": "Подзадачи",
"moveToTop": "Переместить в самый верх",
"deleteTask": "Удалить задачу",
"newSubtask": "Новая подзадача",
- "deleteTaskConfirmation": "Удалить «{title}» из Google Tasks?",
+ "deleteTaskConfirmation": "Удалить «{title}»?",
"metadata": "Метаданные",
"id": "Идентификатор",
"etag": "ETag",
@@ -370,5 +371,201 @@
"selectTimeZone": "Выберите часовой пояс",
"searchLocations": "Поиск города",
"noLocationsFound": "Ничего не найдено",
- "deleteCalendarConfirmation": "Удалить «{title}»?"
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
+ "deleteCalendarConfirmation": "Удалить «{title}»?",
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {
+ "placeholders": {
+ "host": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {
+ "placeholders": {
+ "code": {
+ "type": "String"
+ }
+ }
+ },
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "Срок не может быть раньше начала.",
+ "taskStartDueTimeModeMismatch": "Укажите время начала и срока или сделайте задачу на весь день.",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "unshare": "Закрыть доступ",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "Требуется действие",
+ "taskStatusInProcess": "Выполянется",
+ "taskStatusCompleted": "Завершённые",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "completionDate": "Completion date",
+ "priority": "Приоритет",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "priorityLowValue": "Priority {priority} · Low",
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
+ "reminders": "Reminders",
+ "noReminders": "Напоминаний нет",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "До начала задачи",
+ "beforeTaskDue": "До срока выполнения задачи",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "discardRelatedReminders": "Отменить напоминания",
+ "keepRelatedReminders": "Сохранить напоминания",
+ "repeatEvery": "Повторять каждые",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "Прекратить повтор",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "repeatEveryWeeks": "Every {count} weeks",
+ "repeatEveryMonths": "Every {count} months",
+ "repeatEveryYears": "Every {count} years",
+ "repeatOnDaysSummary": "в {days}",
+ "repeatOnMonthDaysSummary": " {days}",
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "repeatInMonthsSummary": "в {months}",
+ "repeatTimesSummary": "{count} раз",
+ "repeatUntilSummary": "до {date}",
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "duplicateTask": "Дублировать задачу",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "hideSubtasks": "Скрыть вложенные задачи",
+ "hideClosedSubtasks": "Скрыть закрытые подзадачи",
+ "reminderUnit": "Unit"
}
diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb
index 1a0d6a5..8592025 100644
--- a/lib/l10n/app_vi.arb
+++ b/lib/l10n/app_vi.arb
@@ -1,14 +1,14 @@
{
"@@locale": "vi",
"appTitle": "BusyMax",
- "connectGoogleAccount": "Kết nối tài khoản Google và Microsoft để đồng bộ lịch và công việc.",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "Trên màn hình cấp quyền của Google, hãy chọn cả quyền truy cập Lịch và Công việc.",
"googlePermissionsRequiredRetry": "Cần có quyền truy cập Google Calendar và Google Tasks. Vui lòng thử lại và chọn cả hai hộp kiểm.",
"finishSetup": "Hoàn tất thiết lập",
"continueSetup": "Tiếp tục",
"onboardingSetupTitle": "Thiết lập BusyMax",
"onboardingAccountsStepTitle": "Kết nối tài khoản",
- "onboardingAccountsStepDescription": "Thêm tất cả tài khoản Google và Microsoft bạn muốn sử dụng. BusyMax đồng bộ lịch, sự kiện, danh sách công việc và công việc từ mỗi tài khoản.",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "Chọn cài đặt hệ thống",
"onboardingPreferencesStepDescription": "Thiết lập cách ứng dụng hoạt động trên máy tính, lời nhắc, mức độ chi tiết của thông báo và giao diện trước khi mở lịch biểu.",
"signInWithGoogle": "Đăng nhập bằng Google",
@@ -180,13 +180,13 @@
"removingAccount": "Đang xóa tài khoản…",
"removeAccountDescription": "Dừng đồng bộ và xóa dữ liệu của tài khoản này khỏi thiết bị.",
"removeAccountTitle": "Xóa {account} khỏi BusyMax?",
- "removeAccountConfirmation": "Thao tác này sẽ xóa công việc, lịch, sự kiện, lời nhắc đã lưu trong bộ nhớ đệm và các thay đổi ngoại tuyến đang chờ khỏi thiết bị. Các thay đổi chưa đồng bộ sẽ bị mất. Không có dữ liệu nào bị xóa khỏi Google hoặc Microsoft.",
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "Đồng thời thu hồi quyền truy cập của BusyMax vào tài khoản Google này",
"revokeGoogleAccessDescription": "Bạn sẽ cần cấp lại quyền truy cập trước khi kết nối lại.",
"removeAccountAction": "Xóa tài khoản",
"removeAccountFailed": "Không thể hoàn tất việc xóa tài khoản. Hãy thử lại.",
"accountRemovedGoogleRevokeFailed": "Tài khoản đã bị xóa khỏi thiết bị này, nhưng BusyMax không thể thu hồi quyền truy cập vào tài khoản Google. Bạn có thể thu hồi quyền trong phần cài đặt Tài khoản Google.",
- "newList": "Danh sách mới",
+ "newTaskList": "Danh sách công việc mới",
"signInToViewTaskLists": "Đăng nhập để xem danh sách công việc.",
"noTaskListsSynced": "Chưa có danh sách công việc nào được đồng bộ.",
"listActions": "Thao tác với danh sách",
@@ -273,10 +273,11 @@
"list": "Danh sách",
"microsoftMoveUnsupported": "Phiên bản này không hỗ trợ di chuyển công việc giữa các danh sách trong tài khoản Microsoft To Do.",
"createSubtask": "Tạo công việc con",
+ "subtasks": "Công việc con",
"moveToTop": "Chuyển lên đầu",
"deleteTask": "Xóa công việc",
"newSubtask": "Công việc con mới",
- "deleteTaskConfirmation": "Xóa “{title}” khỏi Google Tasks?",
+ "deleteTaskConfirmation": "Xóa “{title}”?",
"metadata": "Siêu dữ liệu",
"id": "ID",
"etag": "ETag",
@@ -370,5 +371,201 @@
"selectTimeZone": "Chọn múi giờ",
"searchLocations": "Tìm kiếm địa điểm",
"noLocationsFound": "Không tìm thấy địa điểm",
- "deleteCalendarConfirmation": "Xóa “{title}”?"
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
+ "deleteCalendarConfirmation": "Xóa “{title}”?",
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {
+ "placeholders": {
+ "host": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {
+ "placeholders": {
+ "code": {
+ "type": "String"
+ }
+ }
+ },
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "Hạn chót không được trước thời gian bắt đầu.",
+ "taskStartDueTimeModeMismatch": "Đặt giờ cho cả thời gian bắt đầu và hạn chót, hoặc đặt công việc là cả ngày.",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "unshare": "Bỏ chia sẽ",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "Needs action",
+ "taskStatusInProcess": "In process",
+ "taskStatusCompleted": "Hoàn thành",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "completionDate": "Completion date",
+ "priority": "Priority",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "priorityLowValue": "Priority {priority} · Low",
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
+ "reminders": "Reminders",
+ "noReminders": "No reminders",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "Before the task starts",
+ "beforeTaskDue": "Before the task is due",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "discardRelatedReminders": "Discard reminders",
+ "keepRelatedReminders": "Keep reminders",
+ "repeatEvery": "Lặp lại mỗi",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "Kết thúc lập lại",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "repeatEveryWeeks": "Every {count} weeks",
+ "repeatEveryMonths": "Every {count} months",
+ "repeatEveryYears": "Every {count} years",
+ "repeatOnDaysSummary": "on {days}",
+ "repeatOnMonthDaysSummary": "on day {days}",
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "repeatInMonthsSummary": "in {months}",
+ "repeatTimesSummary": "{count} lần",
+ "repeatUntilSummary": "cho đến {date}",
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "duplicateTask": "Duplicate task",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "hideSubtasks": "Hide subtasks",
+ "hideClosedSubtasks": "Hide closed subtasks",
+ "reminderUnit": "Unit"
}
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index 71eb8a8..bf244e9 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -1,14 +1,14 @@
{
"@@locale": "zh",
"appTitle": "BusyMax",
- "connectGoogleAccount": "连接 Google 和 Microsoft 帐户以同步日历和任务。",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "在 Google 权限页面上,同时选择日历和任务权限。",
"googlePermissionsRequiredRetry": "必须授予 Google 日历和 Google Tasks 权限。请重试并选中两个复选框。",
"finishSetup": "完成设置",
"continueSetup": "继续",
"onboardingSetupTitle": "设置 BusyMax",
"onboardingAccountsStepTitle": "连接帐户",
- "onboardingAccountsStepDescription": "添加您要使用的所有 Google 和 Microsoft 帐户。BusyMax 会同步每个帐户中的日历、日程、任务列表和任务。",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "选择系统设置",
"onboardingPreferencesStepDescription": "打开日程前,请设置桌面行为、提醒、通知详细程度和外观。",
"signInWithGoogle": "使用 Google 登录",
@@ -180,13 +180,13 @@
"removingAccount": "正在移除帐户…",
"removeAccountDescription": "停止同步并从此设备移除此帐户的数据。",
"removeAccountTitle": "从 BusyMax 中移除 {account}?",
- "removeAccountConfirmation": "这会从此设备删除缓存的任务、日历、日程、提醒和待处理的离线更改。未同步的更改将丢失。不会从 Google 或 Microsoft 删除任何内容。",
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "同时撤销 BusyMax 对此 Google 帐户的访问权限",
"revokeGoogleAccessDescription": "重新连接之前,您需要再次授予访问权限。",
"removeAccountAction": "移除帐户",
"removeAccountFailed": "无法完成帐户移除。请重试。",
"accountRemovedGoogleRevokeFailed": "该帐户已从此设备移除,但无法撤销 BusyMax 对您的 Google 帐户的访问权限。您可以在 Google 帐户中手动撤销该权限。",
- "newList": "新建列表",
+ "newTaskList": "新建任务列表",
"signInToViewTaskLists": "登录以查看任务列表。",
"noTaskListsSynced": "尚未同步任何任务列表。",
"listActions": "列表操作",
@@ -273,10 +273,11 @@
"list": "列表",
"microsoftMoveUnsupported": "此版本不支持在 Microsoft To Do 帐户的列表之间移动任务。",
"createSubtask": "创建子任务",
+ "subtasks": "子任务",
"moveToTop": "移到顶部",
"deleteTask": "删除任务",
"newSubtask": "新建子任务",
- "deleteTaskConfirmation": "从 Google Tasks 中删除“{title}”?",
+ "deleteTaskConfirmation": "删除“{title}”?",
"metadata": "元数据",
"id": "ID",
"etag": "ETag",
@@ -370,5 +371,201 @@
"selectTimeZone": "选择时区",
"searchLocations": "搜索地点",
"noLocationsFound": "未找到地点",
- "deleteCalendarConfirmation": "删除“{title}”?"
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
+ "deleteCalendarConfirmation": "删除“{title}”?",
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {
+ "placeholders": {
+ "host": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {
+ "placeholders": {
+ "code": {
+ "type": "String"
+ }
+ }
+ },
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "截止时间不能早于开始时间。",
+ "taskStartDueTimeModeMismatch": "请同时设置开始和截止时间,或将任务设为全天。",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "unshare": "取消共享",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "需要操作",
+ "taskStatusInProcess": "处理中",
+ "taskStatusCompleted": "完成",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "completionDate": "Completion date",
+ "priority": "优先级",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "priorityLowValue": "Priority {priority} · Low",
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
+ "reminders": "Reminders",
+ "noReminders": "无提醒",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "任务开始前",
+ "beforeTaskDue": "任务截止前",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "discardRelatedReminders": "舍弃提醒",
+ "keepRelatedReminders": "保留提醒",
+ "repeatEvery": "重复每",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "结束重复",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "repeatEveryWeeks": "Every {count} weeks",
+ "repeatEveryMonths": "Every {count} months",
+ "repeatEveryYears": "Every {count} years",
+ "repeatOnDaysSummary": "在 {days}",
+ "repeatOnMonthDaysSummary": "在第 {days} 天",
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "repeatInMonthsSummary": "在 {months}",
+ "repeatTimesSummary": "{count}次",
+ "repeatUntilSummary": "至 {date}",
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "duplicateTask": "复制任务",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "hideSubtasks": "隐藏子任务",
+ "hideClosedSubtasks": "隐藏关闭的子任务",
+ "reminderUnit": "Unit"
}
diff --git a/lib/l10n/app_zh_Hans.arb b/lib/l10n/app_zh_Hans.arb
index 75338a1..060a86d 100644
--- a/lib/l10n/app_zh_Hans.arb
+++ b/lib/l10n/app_zh_Hans.arb
@@ -1,14 +1,14 @@
{
"@@locale": "zh_Hans",
"appTitle": "BusyMax",
- "connectGoogleAccount": "连接 Google 和 Microsoft 帐户以同步日历和任务。",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "在 Google 权限页面上,同时选择日历和任务权限。",
"googlePermissionsRequiredRetry": "必须授予 Google 日历和 Google Tasks 权限。请重试并选中两个复选框。",
"finishSetup": "完成设置",
"continueSetup": "继续",
"onboardingSetupTitle": "设置 BusyMax",
"onboardingAccountsStepTitle": "连接帐户",
- "onboardingAccountsStepDescription": "添加您要使用的所有 Google 和 Microsoft 帐户。BusyMax 会同步每个帐户中的日历、日程、任务列表和任务。",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "选择系统设置",
"onboardingPreferencesStepDescription": "打开日程前,请设置桌面行为、提醒、通知详细程度和外观。",
"signInWithGoogle": "使用 Google 登录",
@@ -180,13 +180,13 @@
"removingAccount": "正在移除帐户…",
"removeAccountDescription": "停止同步并从此设备移除此帐户的数据。",
"removeAccountTitle": "从 BusyMax 中移除 {account}?",
- "removeAccountConfirmation": "这会从此设备删除缓存的任务、日历、日程、提醒和待处理的离线更改。未同步的更改将丢失。不会从 Google 或 Microsoft 删除任何内容。",
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "同时撤销 BusyMax 对此 Google 帐户的访问权限",
"revokeGoogleAccessDescription": "重新连接之前,您需要再次授予访问权限。",
"removeAccountAction": "移除帐户",
"removeAccountFailed": "无法完成帐户移除。请重试。",
"accountRemovedGoogleRevokeFailed": "该帐户已从此设备移除,但无法撤销 BusyMax 对您的 Google 帐户的访问权限。您可以在 Google 帐户中手动撤销该权限。",
- "newList": "新建列表",
+ "newTaskList": "新建任务列表",
"signInToViewTaskLists": "登录以查看任务列表。",
"noTaskListsSynced": "尚未同步任何任务列表。",
"listActions": "列表操作",
@@ -273,10 +273,11 @@
"list": "列表",
"microsoftMoveUnsupported": "此版本不支持在 Microsoft To Do 帐户的列表之间移动任务。",
"createSubtask": "创建子任务",
+ "subtasks": "子任务",
"moveToTop": "移到顶部",
"deleteTask": "删除任务",
"newSubtask": "新建子任务",
- "deleteTaskConfirmation": "从 Google Tasks 中删除“{title}”?",
+ "deleteTaskConfirmation": "删除“{title}”?",
"metadata": "元数据",
"id": "ID",
"etag": "ETag",
@@ -370,5 +371,201 @@
"selectTimeZone": "选择时区",
"searchLocations": "搜索地点",
"noLocationsFound": "未找到地点",
- "deleteCalendarConfirmation": "删除“{title}”?"
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
+ "deleteCalendarConfirmation": "删除“{title}”?",
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {
+ "placeholders": {
+ "host": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {
+ "placeholders": {
+ "code": {
+ "type": "String"
+ }
+ }
+ },
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "截止时间不能早于开始时间。",
+ "taskStartDueTimeModeMismatch": "请同时设置开始和截止时间,或将任务设为全天。",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "unshare": "取消共享",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "需要操作",
+ "taskStatusInProcess": "处理中",
+ "taskStatusCompleted": "完成",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "completionDate": "Completion date",
+ "priority": "优先级",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "priorityLowValue": "Priority {priority} · Low",
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
+ "reminders": "Reminders",
+ "noReminders": "无提醒",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "任务开始前",
+ "beforeTaskDue": "任务截止前",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "discardRelatedReminders": "舍弃提醒",
+ "keepRelatedReminders": "保留提醒",
+ "repeatEvery": "重复每",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "结束重复",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "repeatEveryWeeks": "Every {count} weeks",
+ "repeatEveryMonths": "Every {count} months",
+ "repeatEveryYears": "Every {count} years",
+ "repeatOnDaysSummary": "在 {days}",
+ "repeatOnMonthDaysSummary": "在第 {days} 天",
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "repeatInMonthsSummary": "在 {months}",
+ "repeatTimesSummary": "{count}次",
+ "repeatUntilSummary": "至 {date}",
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "duplicateTask": "复制任务",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "hideSubtasks": "隐藏子任务",
+ "hideClosedSubtasks": "隐藏关闭的子任务",
+ "reminderUnit": "Unit"
}
diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb
index 778d8cc..b798426 100644
--- a/lib/l10n/app_zh_Hant.arb
+++ b/lib/l10n/app_zh_Hant.arb
@@ -1,14 +1,14 @@
{
"@@locale": "zh_Hant",
"appTitle": "BusyMax",
- "connectGoogleAccount": "連結 Google 和 Microsoft 帳戶以同步行事曆和待辦事項。",
+ "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.",
"googlePermissionsConsentNotice": "在 Google 權限畫面中,同時選取行事曆和待辦事項權限。",
"googlePermissionsRequiredRetry": "必須授予 Google 日曆和 Google Tasks 權限。請再試一次並勾選兩個核取方塊。",
"finishSetup": "完成設定",
"continueSetup": "繼續",
"onboardingSetupTitle": "設定 BusyMax",
"onboardingAccountsStepTitle": "連結帳戶",
- "onboardingAccountsStepDescription": "新增您要使用的所有 Google 和 Microsoft 帳戶。BusyMax 會同步每個帳戶中的行事曆、活動、待辦清單和待辦事項。",
+ "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.",
"onboardingPreferencesStepTitle": "選擇系統設定",
"onboardingPreferencesStepDescription": "開啟行程前,請設定桌面行為、提醒、通知詳細程度和外觀。",
"signInWithGoogle": "使用 Google 登入",
@@ -180,13 +180,13 @@
"removingAccount": "正在移除帳戶…",
"removeAccountDescription": "停止同步並從此裝置移除此帳戶的資料。",
"removeAccountTitle": "要從 BusyMax 移除 {account} 嗎?",
- "removeAccountConfirmation": "這會從此裝置刪除快取的待辦事項、行事曆、活動、提醒和待處理的離線變更。未同步的變更將會遺失。不會從 Google 或 Microsoft 刪除任何內容。",
+ "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.",
"revokeGoogleAccess": "同時撤銷 BusyMax 對此 Google 帳戶的存取權",
"revokeGoogleAccessDescription": "重新連結前,您必須再次授予存取權。",
"removeAccountAction": "移除帳戶",
"removeAccountFailed": "無法完成帳戶移除。請再試一次。",
"accountRemovedGoogleRevokeFailed": "該帳戶已從此裝置移除,但無法撤銷 BusyMax 對您的 Google 帳戶的存取權。您可以在 Google 帳戶中手動撤銷該權限。",
- "newList": "新增清單",
+ "newTaskList": "新增待辦清單",
"signInToViewTaskLists": "登入以查看待辦清單。",
"noTaskListsSynced": "尚未同步任何待辦清單。",
"listActions": "清單動作",
@@ -273,10 +273,11 @@
"list": "清單",
"microsoftMoveUnsupported": "此版本不支援在 Microsoft To Do 帳戶的清單之間移動待辦事項。",
"createSubtask": "建立子待辦事項",
+ "subtasks": "子待辦事項",
"moveToTop": "移至頂端",
"deleteTask": "刪除待辦事項",
"newSubtask": "新增子待辦事項",
- "deleteTaskConfirmation": "要從 Google Tasks 刪除「{title}」嗎?",
+ "deleteTaskConfirmation": "要刪除「{title}」嗎?",
"metadata": "中繼資料",
"id": "ID",
"etag": "ETag",
@@ -370,5 +371,201 @@
"selectTimeZone": "選擇時區",
"searchLocations": "搜尋地點",
"noLocationsFound": "找不到地點",
- "deleteCalendarConfirmation": "要刪除「{title}」嗎?"
+ "requiredField": "This field is required.",
+ "providerConnectionDescription": "Connect calendars and tasks from one of these providers.",
+ "appleICloudProvider": "Apple iCloud Calendar",
+ "nextcloudProvider": "Nextcloud",
+ "appleICloudTasksProvider": "Apple iCloud",
+ "nextcloudTasksProvider": "Nextcloud Tasks",
+ "addAppleICloudAccount": "Add Apple iCloud Calendar account",
+ "addNextcloudAccount": "Add Nextcloud account",
+ "waitingForAppleICloud": "Connecting to Apple iCloud…",
+ "waitingForNextcloud": "Waiting for Nextcloud authorization…",
+ "connectAppleICloudTitle": "Connect Apple iCloud Calendar",
+ "appleAccountEmail": "Apple Account email",
+ "appleAppSpecificPassword": "App-specific password",
+ "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.",
+ "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.",
+ "connectNextcloudTitle": "Connect Nextcloud",
+ "nextcloudServerUrl": "Nextcloud server or CalDAV address",
+ "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.",
+ "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.",
+ "connectAccountAction": "Connect",
+ "cancelAccountConnection": "Cancel connection",
+ "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.",
+ "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.",
+ "davReauthenticationRequired": "Reconnect this account to resume synchronization.",
+ "davTemporarilyUnavailable": "This account is temporarily unavailable.",
+ "davPermissionChanged": "Server permissions changed. Pending edits are paused.",
+ "davUnsupportedServer": "This server or provider profile is not supported.",
+ "collectionSettings": "Calendars and task lists",
+ "calendarContent": "Calendar events",
+ "taskContent": "Tasks",
+ "readOnlySharedCollection": "Read-only",
+ "pendingLocally": "Pending locally",
+ "conflictBlocked": "Blocked by conflict",
+ "authenticationBlocked": "Blocked until reconnect",
+ "operationFailed": "Operation failed",
+ "keepServerVersion": "Keep server version",
+ "reapplyLocalChange": "Review and reapply local change",
+ "duplicateLocalItem": "Duplicate as new item",
+ "deleteCalendarConfirmation": "要刪除「{title}」嗎?",
+ "davConnectionState": "Connection state",
+ "davConnected": "Connected",
+ "davConnecting": "Connecting…",
+ "davSignedOut": "Signed out",
+ "davLastSuccessfulSync": "Last successful sync: {time}",
+ "@davLastSuccessfulSync": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "davNeverSynced": "Not synchronized yet",
+ "refreshCollections": "Refresh calendars and task lists",
+ "nextcloudServerHost": "Server: {host}",
+ "@nextcloudServerHost": {
+ "placeholders": {
+ "host": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSupportsEvents": "Event calendar",
+ "collectionSupportsTasks": "Task list",
+ "collectionSupportsEventsAndTasks": "Events and tasks",
+ "writableCollection": "Writable",
+ "sharedCollection": "Shared",
+ "collectionLastSynced": "Last synchronized: {time}",
+ "@collectionLastSynced": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionSyncError": "Sync issue: {code}",
+ "@collectionSyncError": {
+ "placeholders": {
+ "code": {
+ "type": "String"
+ }
+ }
+ },
+ "syncConflicts": "Synchronization conflicts",
+ "remoteChangedAt": "Server changed: {time}",
+ "@remoteChangedAt": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "localPendingEdit": "Local edit: {summary}",
+ "@localPendingEdit": {
+ "placeholders": {
+ "summary": {
+ "type": "String"
+ }
+ }
+ },
+ "conflictResolutionFailed": "The conflict could not be resolved.",
+ "recurringEventScope": "Recurring event scope",
+ "entireSeries": "Entire series",
+ "singleOccurrence": "This occurrence",
+ "thisAndFutureUnavailable": "This and future (not available)",
+ "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.",
+ "taskDueBeforeStart": "到期時間不得早於開始時間。",
+ "taskStartDueTimeModeMismatch": "請同時設定開始與到期時間,或將工作設為全天。",
+ "taskListCreateFailed": "Could not create the task list: {error}",
+ "taskListRenameFailed": "Could not rename the task list: {error}",
+ "taskListDeleteFailed": "Could not delete the task list: {error}",
+ "unshare": "撤回分享",
+ "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.",
+ "taskListCannotDelete": "This task list cannot be deleted with your current permissions.",
+ "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?",
+ "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?",
+ "taskStatus": "Status",
+ "taskStatusNone": "No status",
+ "taskStatusNeedsAction": "需要動作",
+ "taskStatusInProcess": "進行中",
+ "taskStatusCompleted": "完成",
+ "taskStatusCancelled": "Cancelled",
+ "completionPercent": "{percent}% completed",
+ "completionDate": "Completion date",
+ "priority": "優先",
+ "priorityNone": "No priority",
+ "priorityHighValue": "Priority {priority} · High",
+ "priorityMediumValue": "Priority {priority} · Medium",
+ "priorityLowValue": "Priority {priority} · Low",
+ "taskUrl": "URL",
+ "invalidTaskUrl": "Enter an absolute URL, including its scheme.",
+ "classification": "Classification",
+ "classificationPublic": "When shared, show the full task",
+ "classificationConfidential": "When shared, show only busy",
+ "classificationPrivate": "When shared, hide this task",
+ "pinTask": "Pin task",
+ "reminders": "Reminders",
+ "noReminders": "無提醒",
+ "editReminder": "Edit reminder",
+ "beforeTaskStarts": "在任務開始前",
+ "beforeTaskDue": "任務截止前",
+ "afterTaskStarts": "After the task starts",
+ "afterTaskDue": "After the task is due",
+ "relativeToTaskStart": "Relative to the task start date",
+ "relativeToTaskDue": "Relative to the task due date",
+ "reminderTimeOfDay": "Time of day",
+ "absoluteReminder": "At a date and time",
+ "reminderAmount": "Amount",
+ "reminderUnitSeconds": "Seconds",
+ "reminderUnitMinutes": "Minutes",
+ "reminderUnitHours": "Hours",
+ "reminderUnitDays": "Days",
+ "reminderUnitWeeks": "Weeks",
+ "reminderAtTaskStart": "At the task start",
+ "reminderAtTaskDue": "At the task due time",
+ "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.",
+ "relatedRemindersTitle": "Keep related reminders?",
+ "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?",
+ "discardRelatedReminders": "捨棄提醒",
+ "keepRelatedReminders": "保留提醒",
+ "repeatEvery": "重複循環",
+ "repeatOn": "Repeat on",
+ "repeatEnd": "停止重複",
+ "repeatNever": "Never",
+ "repeatUntil": "On date",
+ "repeatAfter": "After a number of occurrences",
+ "repeatCount": "Occurrences",
+ "repeatDayOfMonth": "Days of month",
+ "repeatMonths": "Months",
+ "repeatOrdinal": "Weekday position",
+ "repeatSpecificDays": "Specific days",
+ "repeatFirst": "First",
+ "repeatSecond": "Second",
+ "repeatThird": "Third",
+ "repeatFourth": "Fourth",
+ "repeatFifth": "Fifth",
+ "repeatSecondToLast": "Second to last",
+ "repeatLast": "Last",
+ "repeatAnyDay": "Day",
+ "repeatWeekday": "Weekday",
+ "repeatWeekendDay": "Weekend day",
+ "repeatEveryDays": "Every {count} days",
+ "repeatEveryWeeks": "Every {count} weeks",
+ "repeatEveryMonths": "Every {count} months",
+ "repeatEveryYears": "Every {count} years",
+ "repeatOnDaysSummary": "於 {days}",
+ "repeatOnMonthDaysSummary": "於 {days} 天",
+ "repeatOnOrdinalSummary": "on the {ordinal} {days}",
+ "repeatInMonthsSummary": "在 {months}",
+ "repeatTimesSummary": "{count} 次",
+ "repeatUntilSummary": "到 {date}",
+ "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "duplicateTask": "再製任務",
+ "taskDuplicated": "Task duplicated.",
+ "taskDuplicateFailed": "Could not duplicate the task: {error}",
+ "hideSubtasks": "隱藏子工作項目",
+ "hideClosedSubtasks": "隱藏已關閉的子工作項目",
+ "reminderUnit": "Unit"
}
diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart
index 77b8b4c..67b9d2e 100644
--- a/lib/l10n/generated/app_localizations.dart
+++ b/lib/l10n/generated/app_localizations.dart
@@ -141,7 +141,7 @@ abstract class AppLocalizations {
/// No description provided for @connectGoogleAccount.
///
/// In en, this message translates to:
- /// **'Connect Google and Microsoft accounts to sync calendars and tasks.'**
+ /// **'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'**
String get connectGoogleAccount;
/// No description provided for @googlePermissionsConsentNotice.
@@ -183,7 +183,7 @@ abstract class AppLocalizations {
/// No description provided for @onboardingAccountsStepDescription.
///
/// In en, this message translates to:
- /// **'Add all Google and Microsoft accounts you want to use. BusyMax syncs calendars, events, task lists, and tasks from each account.'**
+ /// **'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'**
String get onboardingAccountsStepDescription;
/// No description provided for @onboardingPreferencesStepTitle.
@@ -1215,7 +1215,7 @@ abstract class AppLocalizations {
/// No description provided for @removeAccountConfirmation.
///
/// In en, this message translates to:
- /// **'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Nothing will be deleted from Google or Microsoft.'**
+ /// **'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'**
String get removeAccountConfirmation;
/// No description provided for @revokeGoogleAccess.
@@ -1248,11 +1248,29 @@ abstract class AppLocalizations {
/// **'The account was removed from this device, but BusyMax could not revoke Google access. You can revoke it from your Google Account.'**
String get accountRemovedGoogleRevokeFailed;
- /// No description provided for @newList.
+ /// No description provided for @newTaskList.
///
/// In en, this message translates to:
- /// **'New list'**
- String get newList;
+ /// **'New task list'**
+ String get newTaskList;
+
+ /// No description provided for @taskListCreateFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'Could not create the task list: {error}'**
+ String taskListCreateFailed(String error);
+
+ /// No description provided for @taskListRenameFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'Could not rename the task list: {error}'**
+ String taskListRenameFailed(String error);
+
+ /// No description provided for @taskListDeleteFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'Could not delete the task list: {error}'**
+ String taskListDeleteFailed(String error);
/// No description provided for @signInToViewTaskLists.
///
@@ -1296,6 +1314,24 @@ abstract class AppLocalizations {
/// **'Delete list'**
String get deleteList;
+ /// No description provided for @unshare.
+ ///
+ /// In en, this message translates to:
+ /// **'Unshare'**
+ String get unshare;
+
+ /// No description provided for @readOnlyTaskListCannotRename.
+ ///
+ /// In en, this message translates to:
+ /// **'This task list is read-only and cannot be renamed.'**
+ String get readOnlyTaskListCannotRename;
+
+ /// No description provided for @taskListCannotDelete.
+ ///
+ /// In en, this message translates to:
+ /// **'This task list cannot be deleted with your current permissions.'**
+ String get taskListCannotDelete;
+
/// No description provided for @builtInMicrosoftList.
///
/// In en, this message translates to:
@@ -1314,6 +1350,18 @@ abstract class AppLocalizations {
/// **'Delete \"{title}\" from Google Tasks?'**
String deleteListConfirmation(String title);
+ /// No description provided for @deleteTaskListConfirmation.
+ ///
+ /// In en, this message translates to:
+ /// **'Delete \"{title}\" and all of its tasks?'**
+ String deleteTaskListConfirmation(String title);
+
+ /// No description provided for @unshareTaskListConfirmation.
+ ///
+ /// In en, this message translates to:
+ /// **'Unshare \"{title}\" from this account?'**
+ String unshareTaskListConfirmation(String title);
+
/// No description provided for @deleteEvent.
///
/// In en, this message translates to:
@@ -1536,6 +1584,126 @@ abstract class AppLocalizations {
/// **'Done'**
String get doneStatus;
+ /// No description provided for @taskStatus.
+ ///
+ /// In en, this message translates to:
+ /// **'Status'**
+ String get taskStatus;
+
+ /// No description provided for @taskStatusNone.
+ ///
+ /// In en, this message translates to:
+ /// **'No status'**
+ String get taskStatusNone;
+
+ /// No description provided for @taskStatusNeedsAction.
+ ///
+ /// In en, this message translates to:
+ /// **'Needs action'**
+ String get taskStatusNeedsAction;
+
+ /// No description provided for @taskStatusInProcess.
+ ///
+ /// In en, this message translates to:
+ /// **'In process'**
+ String get taskStatusInProcess;
+
+ /// No description provided for @taskStatusCompleted.
+ ///
+ /// In en, this message translates to:
+ /// **'Completed'**
+ String get taskStatusCompleted;
+
+ /// No description provided for @taskStatusCancelled.
+ ///
+ /// In en, this message translates to:
+ /// **'Cancelled'**
+ String get taskStatusCancelled;
+
+ /// No description provided for @completionPercent.
+ ///
+ /// In en, this message translates to:
+ /// **'{percent}% completed'**
+ String completionPercent(int percent);
+
+ /// No description provided for @completionDate.
+ ///
+ /// In en, this message translates to:
+ /// **'Completion date'**
+ String get completionDate;
+
+ /// No description provided for @priority.
+ ///
+ /// In en, this message translates to:
+ /// **'Priority'**
+ String get priority;
+
+ /// No description provided for @priorityNone.
+ ///
+ /// In en, this message translates to:
+ /// **'No priority'**
+ String get priorityNone;
+
+ /// No description provided for @priorityHighValue.
+ ///
+ /// In en, this message translates to:
+ /// **'Priority {priority} · High'**
+ String priorityHighValue(int priority);
+
+ /// No description provided for @priorityMediumValue.
+ ///
+ /// In en, this message translates to:
+ /// **'Priority {priority} · Medium'**
+ String priorityMediumValue(int priority);
+
+ /// No description provided for @priorityLowValue.
+ ///
+ /// In en, this message translates to:
+ /// **'Priority {priority} · Low'**
+ String priorityLowValue(int priority);
+
+ /// No description provided for @taskUrl.
+ ///
+ /// In en, this message translates to:
+ /// **'URL'**
+ String get taskUrl;
+
+ /// No description provided for @invalidTaskUrl.
+ ///
+ /// In en, this message translates to:
+ /// **'Enter an absolute URL, including its scheme.'**
+ String get invalidTaskUrl;
+
+ /// No description provided for @classification.
+ ///
+ /// In en, this message translates to:
+ /// **'Classification'**
+ String get classification;
+
+ /// No description provided for @classificationPublic.
+ ///
+ /// In en, this message translates to:
+ /// **'When shared, show the full task'**
+ String get classificationPublic;
+
+ /// No description provided for @classificationConfidential.
+ ///
+ /// In en, this message translates to:
+ /// **'When shared, show only busy'**
+ String get classificationConfidential;
+
+ /// No description provided for @classificationPrivate.
+ ///
+ /// In en, this message translates to:
+ /// **'When shared, hide this task'**
+ String get classificationPrivate;
+
+ /// No description provided for @pinTask.
+ ///
+ /// In en, this message translates to:
+ /// **'Pin task'**
+ String get pinTask;
+
/// No description provided for @notes.
///
/// In en, this message translates to:
@@ -1608,6 +1776,156 @@ abstract class AppLocalizations {
/// **'Add Reminder'**
String get addReminder;
+ /// No description provided for @reminders.
+ ///
+ /// In en, this message translates to:
+ /// **'Reminders'**
+ String get reminders;
+
+ /// No description provided for @noReminders.
+ ///
+ /// In en, this message translates to:
+ /// **'No reminders'**
+ String get noReminders;
+
+ /// No description provided for @editReminder.
+ ///
+ /// In en, this message translates to:
+ /// **'Edit reminder'**
+ String get editReminder;
+
+ /// No description provided for @beforeTaskStarts.
+ ///
+ /// In en, this message translates to:
+ /// **'Before the task starts'**
+ String get beforeTaskStarts;
+
+ /// No description provided for @beforeTaskDue.
+ ///
+ /// In en, this message translates to:
+ /// **'Before the task is due'**
+ String get beforeTaskDue;
+
+ /// No description provided for @afterTaskStarts.
+ ///
+ /// In en, this message translates to:
+ /// **'After the task starts'**
+ String get afterTaskStarts;
+
+ /// No description provided for @afterTaskDue.
+ ///
+ /// In en, this message translates to:
+ /// **'After the task is due'**
+ String get afterTaskDue;
+
+ /// No description provided for @relativeToTaskStart.
+ ///
+ /// In en, this message translates to:
+ /// **'Relative to the task start date'**
+ String get relativeToTaskStart;
+
+ /// No description provided for @relativeToTaskDue.
+ ///
+ /// In en, this message translates to:
+ /// **'Relative to the task due date'**
+ String get relativeToTaskDue;
+
+ /// No description provided for @reminderTimeOfDay.
+ ///
+ /// In en, this message translates to:
+ /// **'Time of day'**
+ String get reminderTimeOfDay;
+
+ /// No description provided for @absoluteReminder.
+ ///
+ /// In en, this message translates to:
+ /// **'At a date and time'**
+ String get absoluteReminder;
+
+ /// No description provided for @reminderAmount.
+ ///
+ /// In en, this message translates to:
+ /// **'Amount'**
+ String get reminderAmount;
+
+ /// No description provided for @reminderUnit.
+ ///
+ /// In en, this message translates to:
+ /// **'Unit'**
+ String get reminderUnit;
+
+ /// No description provided for @reminderUnitSeconds.
+ ///
+ /// In en, this message translates to:
+ /// **'Seconds'**
+ String get reminderUnitSeconds;
+
+ /// No description provided for @reminderUnitMinutes.
+ ///
+ /// In en, this message translates to:
+ /// **'Minutes'**
+ String get reminderUnitMinutes;
+
+ /// No description provided for @reminderUnitHours.
+ ///
+ /// In en, this message translates to:
+ /// **'Hours'**
+ String get reminderUnitHours;
+
+ /// No description provided for @reminderUnitDays.
+ ///
+ /// In en, this message translates to:
+ /// **'Days'**
+ String get reminderUnitDays;
+
+ /// No description provided for @reminderUnitWeeks.
+ ///
+ /// In en, this message translates to:
+ /// **'Weeks'**
+ String get reminderUnitWeeks;
+
+ /// No description provided for @reminderAtTaskStart.
+ ///
+ /// In en, this message translates to:
+ /// **'At the task start'**
+ String get reminderAtTaskStart;
+
+ /// No description provided for @reminderAtTaskDue.
+ ///
+ /// In en, this message translates to:
+ /// **'At the task due time'**
+ String get reminderAtTaskDue;
+
+ /// No description provided for @unsupportedReminder.
+ ///
+ /// In en, this message translates to:
+ /// **'This reminder type is preserved but its time cannot be edited.'**
+ String get unsupportedReminder;
+
+ /// No description provided for @relatedRemindersTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Keep related reminders?'**
+ String get relatedRemindersTitle;
+
+ /// No description provided for @relatedRemindersDescription.
+ ///
+ /// In en, this message translates to:
+ /// **'This date has {count} related reminders. Keep them at their current date and time?'**
+ String relatedRemindersDescription(int count);
+
+ /// No description provided for @discardRelatedReminders.
+ ///
+ /// In en, this message translates to:
+ /// **'Discard reminders'**
+ String get discardRelatedReminders;
+
+ /// No description provided for @keepRelatedReminders.
+ ///
+ /// In en, this message translates to:
+ /// **'Keep reminders'**
+ String get keepRelatedReminders;
+
/// No description provided for @addGuest.
///
/// In en, this message translates to:
@@ -1674,6 +1992,198 @@ abstract class AppLocalizations {
/// **'Yearly'**
String get repeatYearly;
+ /// No description provided for @repeatEvery.
+ ///
+ /// In en, this message translates to:
+ /// **'Repeat every'**
+ String get repeatEvery;
+
+ /// No description provided for @repeatOn.
+ ///
+ /// In en, this message translates to:
+ /// **'Repeat on'**
+ String get repeatOn;
+
+ /// No description provided for @repeatEnd.
+ ///
+ /// In en, this message translates to:
+ /// **'End repeat'**
+ String get repeatEnd;
+
+ /// No description provided for @repeatNever.
+ ///
+ /// In en, this message translates to:
+ /// **'Never'**
+ String get repeatNever;
+
+ /// No description provided for @repeatUntil.
+ ///
+ /// In en, this message translates to:
+ /// **'On date'**
+ String get repeatUntil;
+
+ /// No description provided for @repeatAfter.
+ ///
+ /// In en, this message translates to:
+ /// **'After a number of occurrences'**
+ String get repeatAfter;
+
+ /// No description provided for @repeatCount.
+ ///
+ /// In en, this message translates to:
+ /// **'Occurrences'**
+ String get repeatCount;
+
+ /// No description provided for @repeatDayOfMonth.
+ ///
+ /// In en, this message translates to:
+ /// **'Days of month'**
+ String get repeatDayOfMonth;
+
+ /// No description provided for @repeatMonths.
+ ///
+ /// In en, this message translates to:
+ /// **'Months'**
+ String get repeatMonths;
+
+ /// No description provided for @repeatOrdinal.
+ ///
+ /// In en, this message translates to:
+ /// **'Weekday position'**
+ String get repeatOrdinal;
+
+ /// No description provided for @repeatSpecificDays.
+ ///
+ /// In en, this message translates to:
+ /// **'Specific days'**
+ String get repeatSpecificDays;
+
+ /// No description provided for @repeatFirst.
+ ///
+ /// In en, this message translates to:
+ /// **'First'**
+ String get repeatFirst;
+
+ /// No description provided for @repeatSecond.
+ ///
+ /// In en, this message translates to:
+ /// **'Second'**
+ String get repeatSecond;
+
+ /// No description provided for @repeatThird.
+ ///
+ /// In en, this message translates to:
+ /// **'Third'**
+ String get repeatThird;
+
+ /// No description provided for @repeatFourth.
+ ///
+ /// In en, this message translates to:
+ /// **'Fourth'**
+ String get repeatFourth;
+
+ /// No description provided for @repeatFifth.
+ ///
+ /// In en, this message translates to:
+ /// **'Fifth'**
+ String get repeatFifth;
+
+ /// No description provided for @repeatSecondToLast.
+ ///
+ /// In en, this message translates to:
+ /// **'Second to last'**
+ String get repeatSecondToLast;
+
+ /// No description provided for @repeatLast.
+ ///
+ /// In en, this message translates to:
+ /// **'Last'**
+ String get repeatLast;
+
+ /// No description provided for @repeatAnyDay.
+ ///
+ /// In en, this message translates to:
+ /// **'Day'**
+ String get repeatAnyDay;
+
+ /// No description provided for @repeatWeekday.
+ ///
+ /// In en, this message translates to:
+ /// **'Weekday'**
+ String get repeatWeekday;
+
+ /// No description provided for @repeatWeekendDay.
+ ///
+ /// In en, this message translates to:
+ /// **'Weekend day'**
+ String get repeatWeekendDay;
+
+ /// No description provided for @repeatEveryDays.
+ ///
+ /// In en, this message translates to:
+ /// **'Every {count} days'**
+ String repeatEveryDays(int count);
+
+ /// No description provided for @repeatEveryWeeks.
+ ///
+ /// In en, this message translates to:
+ /// **'Every {count} weeks'**
+ String repeatEveryWeeks(int count);
+
+ /// No description provided for @repeatEveryMonths.
+ ///
+ /// In en, this message translates to:
+ /// **'Every {count} months'**
+ String repeatEveryMonths(int count);
+
+ /// No description provided for @repeatEveryYears.
+ ///
+ /// In en, this message translates to:
+ /// **'Every {count} years'**
+ String repeatEveryYears(int count);
+
+ /// No description provided for @repeatOnDaysSummary.
+ ///
+ /// In en, this message translates to:
+ /// **'on {days}'**
+ String repeatOnDaysSummary(String days);
+
+ /// No description provided for @repeatOnMonthDaysSummary.
+ ///
+ /// In en, this message translates to:
+ /// **'on day {days}'**
+ String repeatOnMonthDaysSummary(String days);
+
+ /// No description provided for @repeatOnOrdinalSummary.
+ ///
+ /// In en, this message translates to:
+ /// **'on the {ordinal} {days}'**
+ String repeatOnOrdinalSummary(String ordinal, String days);
+
+ /// No description provided for @repeatInMonthsSummary.
+ ///
+ /// In en, this message translates to:
+ /// **'in {months}'**
+ String repeatInMonthsSummary(String months);
+
+ /// No description provided for @repeatTimesSummary.
+ ///
+ /// In en, this message translates to:
+ /// **'{count} times'**
+ String repeatTimesSummary(int count);
+
+ /// No description provided for @repeatUntilSummary.
+ ///
+ /// In en, this message translates to:
+ /// **'until {date}'**
+ String repeatUntilSummary(String date);
+
+ /// No description provided for @unsupportedRecurrencePreserved.
+ ///
+ /// In en, this message translates to:
+ /// **'This recurrence rule uses options that this editor does not change.'**
+ String get unsupportedRecurrencePreserved;
+
/// No description provided for @importance.
///
/// In en, this message translates to:
@@ -1770,6 +2280,42 @@ abstract class AppLocalizations {
/// **'Create subtask'**
String get createSubtask;
+ /// No description provided for @subtasks.
+ ///
+ /// In en, this message translates to:
+ /// **'Subtasks'**
+ String get subtasks;
+
+ /// No description provided for @duplicateTask.
+ ///
+ /// In en, this message translates to:
+ /// **'Duplicate task'**
+ String get duplicateTask;
+
+ /// No description provided for @taskDuplicated.
+ ///
+ /// In en, this message translates to:
+ /// **'Task duplicated.'**
+ String get taskDuplicated;
+
+ /// No description provided for @taskDuplicateFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'Could not duplicate the task: {error}'**
+ String taskDuplicateFailed(String error);
+
+ /// No description provided for @hideSubtasks.
+ ///
+ /// In en, this message translates to:
+ /// **'Hide subtasks'**
+ String get hideSubtasks;
+
+ /// No description provided for @hideClosedSubtasks.
+ ///
+ /// In en, this message translates to:
+ /// **'Hide closed subtasks'**
+ String get hideClosedSubtasks;
+
/// No description provided for @moveToTop.
///
/// In en, this message translates to:
@@ -1791,7 +2337,7 @@ abstract class AppLocalizations {
/// No description provided for @deleteTaskConfirmation.
///
/// In en, this message translates to:
- /// **'Delete \"{title}\" from Google Tasks?'**
+ /// **'Delete \"{title}\"?'**
String deleteTaskConfirmation(String title);
/// No description provided for @metadata.
@@ -2352,6 +2898,390 @@ abstract class AppLocalizations {
/// **'No locations found'**
String get noLocationsFound;
+ /// No description provided for @requiredField.
+ ///
+ /// In en, this message translates to:
+ /// **'This field is required.'**
+ String get requiredField;
+
+ /// No description provided for @providerConnectionDescription.
+ ///
+ /// In en, this message translates to:
+ /// **'Connect calendars and tasks from one of these providers.'**
+ String get providerConnectionDescription;
+
+ /// No description provided for @appleICloudProvider.
+ ///
+ /// In en, this message translates to:
+ /// **'Apple iCloud Calendar'**
+ String get appleICloudProvider;
+
+ /// No description provided for @nextcloudProvider.
+ ///
+ /// In en, this message translates to:
+ /// **'Nextcloud'**
+ String get nextcloudProvider;
+
+ /// No description provided for @appleICloudTasksProvider.
+ ///
+ /// In en, this message translates to:
+ /// **'Apple iCloud'**
+ String get appleICloudTasksProvider;
+
+ /// No description provided for @nextcloudTasksProvider.
+ ///
+ /// In en, this message translates to:
+ /// **'Nextcloud Tasks'**
+ String get nextcloudTasksProvider;
+
+ /// No description provided for @addAppleICloudAccount.
+ ///
+ /// In en, this message translates to:
+ /// **'Add Apple iCloud Calendar account'**
+ String get addAppleICloudAccount;
+
+ /// No description provided for @addNextcloudAccount.
+ ///
+ /// In en, this message translates to:
+ /// **'Add Nextcloud account'**
+ String get addNextcloudAccount;
+
+ /// No description provided for @waitingForAppleICloud.
+ ///
+ /// In en, this message translates to:
+ /// **'Connecting to Apple iCloud…'**
+ String get waitingForAppleICloud;
+
+ /// No description provided for @waitingForNextcloud.
+ ///
+ /// In en, this message translates to:
+ /// **'Waiting for Nextcloud authorization…'**
+ String get waitingForNextcloud;
+
+ /// No description provided for @connectAppleICloudTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Connect Apple iCloud Calendar'**
+ String get connectAppleICloudTitle;
+
+ /// No description provided for @appleAccountEmail.
+ ///
+ /// In en, this message translates to:
+ /// **'Apple Account email'**
+ String get appleAccountEmail;
+
+ /// No description provided for @appleAppSpecificPassword.
+ ///
+ /// In en, this message translates to:
+ /// **'App-specific password'**
+ String get appleAppSpecificPassword;
+
+ /// No description provided for @appleAppSpecificPasswordHelp.
+ ///
+ /// In en, this message translates to:
+ /// **'Create an app-specific password after enabling two-factor authentication for your Apple Account.'**
+ String get appleAppSpecificPasswordHelp;
+
+ /// No description provided for @appleAppSpecificPasswordResetWarning.
+ ///
+ /// In en, this message translates to:
+ /// **'Resetting your Apple Account password revokes app-specific passwords.'**
+ String get appleAppSpecificPasswordResetWarning;
+
+ /// No description provided for @connectNextcloudTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Connect Nextcloud'**
+ String get connectNextcloudTitle;
+
+ /// No description provided for @nextcloudServerUrl.
+ ///
+ /// In en, this message translates to:
+ /// **'Nextcloud server or CalDAV address'**
+ String get nextcloudServerUrl;
+
+ /// No description provided for @nextcloudServerUrlHelp.
+ ///
+ /// In en, this message translates to:
+ /// **'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'**
+ String get nextcloudServerUrlHelp;
+
+ /// No description provided for @nextcloudBrowserAuthorizationHelp.
+ ///
+ /// In en, this message translates to:
+ /// **'BusyMax will open your browser. Approve access there, then return to BusyMax.'**
+ String get nextcloudBrowserAuthorizationHelp;
+
+ /// No description provided for @connectAccountAction.
+ ///
+ /// In en, this message translates to:
+ /// **'Connect'**
+ String get connectAccountAction;
+
+ /// No description provided for @cancelAccountConnection.
+ ///
+ /// In en, this message translates to:
+ /// **'Cancel connection'**
+ String get cancelAccountConnection;
+
+ /// No description provided for @nextcloudAccountRemovedRevokeFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'The account was removed locally, but its Nextcloud app password could not be revoked.'**
+ String get nextcloudAccountRemovedRevokeFailed;
+
+ /// No description provided for @davCachedOfflineNotice.
+ ///
+ /// In en, this message translates to:
+ /// **'Calendar and task data is cached locally for offline use.'**
+ String get davCachedOfflineNotice;
+
+ /// No description provided for @davReauthenticationRequired.
+ ///
+ /// In en, this message translates to:
+ /// **'Reconnect this account to resume synchronization.'**
+ String get davReauthenticationRequired;
+
+ /// No description provided for @davTemporarilyUnavailable.
+ ///
+ /// In en, this message translates to:
+ /// **'This account is temporarily unavailable.'**
+ String get davTemporarilyUnavailable;
+
+ /// No description provided for @davPermissionChanged.
+ ///
+ /// In en, this message translates to:
+ /// **'Server permissions changed. Pending edits are paused.'**
+ String get davPermissionChanged;
+
+ /// No description provided for @davUnsupportedServer.
+ ///
+ /// In en, this message translates to:
+ /// **'This server or provider profile is not supported.'**
+ String get davUnsupportedServer;
+
+ /// No description provided for @collectionSettings.
+ ///
+ /// In en, this message translates to:
+ /// **'Calendars and task lists'**
+ String get collectionSettings;
+
+ /// No description provided for @calendarContent.
+ ///
+ /// In en, this message translates to:
+ /// **'Calendar events'**
+ String get calendarContent;
+
+ /// No description provided for @taskContent.
+ ///
+ /// In en, this message translates to:
+ /// **'Tasks'**
+ String get taskContent;
+
+ /// No description provided for @readOnlySharedCollection.
+ ///
+ /// In en, this message translates to:
+ /// **'Read-only'**
+ String get readOnlySharedCollection;
+
+ /// No description provided for @pendingLocally.
+ ///
+ /// In en, this message translates to:
+ /// **'Pending locally'**
+ String get pendingLocally;
+
+ /// No description provided for @conflictBlocked.
+ ///
+ /// In en, this message translates to:
+ /// **'Blocked by conflict'**
+ String get conflictBlocked;
+
+ /// No description provided for @authenticationBlocked.
+ ///
+ /// In en, this message translates to:
+ /// **'Blocked until reconnect'**
+ String get authenticationBlocked;
+
+ /// No description provided for @operationFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'Operation failed'**
+ String get operationFailed;
+
+ /// No description provided for @keepServerVersion.
+ ///
+ /// In en, this message translates to:
+ /// **'Keep server version'**
+ String get keepServerVersion;
+
+ /// No description provided for @reapplyLocalChange.
+ ///
+ /// In en, this message translates to:
+ /// **'Review and reapply local change'**
+ String get reapplyLocalChange;
+
+ /// No description provided for @duplicateLocalItem.
+ ///
+ /// In en, this message translates to:
+ /// **'Duplicate as new item'**
+ String get duplicateLocalItem;
+
+ /// No description provided for @davConnectionState.
+ ///
+ /// In en, this message translates to:
+ /// **'Connection state'**
+ String get davConnectionState;
+
+ /// No description provided for @davConnected.
+ ///
+ /// In en, this message translates to:
+ /// **'Connected'**
+ String get davConnected;
+
+ /// No description provided for @davConnecting.
+ ///
+ /// In en, this message translates to:
+ /// **'Connecting…'**
+ String get davConnecting;
+
+ /// No description provided for @davSignedOut.
+ ///
+ /// In en, this message translates to:
+ /// **'Signed out'**
+ String get davSignedOut;
+
+ /// No description provided for @davLastSuccessfulSync.
+ ///
+ /// In en, this message translates to:
+ /// **'Last successful sync: {time}'**
+ String davLastSuccessfulSync(String time);
+
+ /// No description provided for @davNeverSynced.
+ ///
+ /// In en, this message translates to:
+ /// **'Not synchronized yet'**
+ String get davNeverSynced;
+
+ /// No description provided for @refreshCollections.
+ ///
+ /// In en, this message translates to:
+ /// **'Refresh calendars and task lists'**
+ String get refreshCollections;
+
+ /// No description provided for @nextcloudServerHost.
+ ///
+ /// In en, this message translates to:
+ /// **'Server: {host}'**
+ String nextcloudServerHost(String host);
+
+ /// No description provided for @collectionSupportsEvents.
+ ///
+ /// In en, this message translates to:
+ /// **'Event calendar'**
+ String get collectionSupportsEvents;
+
+ /// No description provided for @collectionSupportsTasks.
+ ///
+ /// In en, this message translates to:
+ /// **'Task list'**
+ String get collectionSupportsTasks;
+
+ /// No description provided for @collectionSupportsEventsAndTasks.
+ ///
+ /// In en, this message translates to:
+ /// **'Events and tasks'**
+ String get collectionSupportsEventsAndTasks;
+
+ /// No description provided for @writableCollection.
+ ///
+ /// In en, this message translates to:
+ /// **'Writable'**
+ String get writableCollection;
+
+ /// No description provided for @sharedCollection.
+ ///
+ /// In en, this message translates to:
+ /// **'Shared'**
+ String get sharedCollection;
+
+ /// No description provided for @collectionLastSynced.
+ ///
+ /// In en, this message translates to:
+ /// **'Last synchronized: {time}'**
+ String collectionLastSynced(String time);
+
+ /// No description provided for @collectionSyncError.
+ ///
+ /// In en, this message translates to:
+ /// **'Sync issue: {code}'**
+ String collectionSyncError(String code);
+
+ /// No description provided for @syncConflicts.
+ ///
+ /// In en, this message translates to:
+ /// **'Synchronization conflicts'**
+ String get syncConflicts;
+
+ /// No description provided for @remoteChangedAt.
+ ///
+ /// In en, this message translates to:
+ /// **'Server changed: {time}'**
+ String remoteChangedAt(String time);
+
+ /// No description provided for @localPendingEdit.
+ ///
+ /// In en, this message translates to:
+ /// **'Local edit: {summary}'**
+ String localPendingEdit(String summary);
+
+ /// No description provided for @conflictResolutionFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'The conflict could not be resolved.'**
+ String get conflictResolutionFailed;
+
+ /// No description provided for @recurringEventScope.
+ ///
+ /// In en, this message translates to:
+ /// **'Recurring event scope'**
+ String get recurringEventScope;
+
+ /// No description provided for @entireSeries.
+ ///
+ /// In en, this message translates to:
+ /// **'Entire series'**
+ String get entireSeries;
+
+ /// No description provided for @singleOccurrence.
+ ///
+ /// In en, this message translates to:
+ /// **'This occurrence'**
+ String get singleOccurrence;
+
+ /// No description provided for @thisAndFutureUnavailable.
+ ///
+ /// In en, this message translates to:
+ /// **'This and future (not available)'**
+ String get thisAndFutureUnavailable;
+
+ /// No description provided for @chooseRecurringEventScope.
+ ///
+ /// In en, this message translates to:
+ /// **'Choose whether this change applies to the entire series or only this occurrence.'**
+ String get chooseRecurringEventScope;
+
+ /// No description provided for @taskDueBeforeStart.
+ ///
+ /// In en, this message translates to:
+ /// **'Due must not be before start.'**
+ String get taskDueBeforeStart;
+
+ /// No description provided for @taskStartDueTimeModeMismatch.
+ ///
+ /// In en, this message translates to:
+ /// **'Set times for both start and due, or make the task all day.'**
+ String get taskStartDueTimeModeMismatch;
+
/// No description provided for @deleteCalendarConfirmation.
///
/// In en, this message translates to:
diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart
index f65376c..fbb898e 100644
--- a/lib/l10n/generated/app_localizations_ar.dart
+++ b/lib/l10n/generated/app_localizations_ar.dart
@@ -17,7 +17,7 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get connectGoogleAccount =>
- 'اربط حسابات Google وMicrosoft لمزامنة التقويمات والمهام.';
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
@override
String get googlePermissionsConsentNotice =>
@@ -41,7 +41,7 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get onboardingAccountsStepDescription =>
- 'أضف جميع حسابات Google وMicrosoft التي تريد استخدامها. يزامن BusyMax التقويمات والأحداث وقوائم المهام والمهام من كل حساب.';
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
@override
String get onboardingPreferencesStepTitle => 'اختيار إعدادات النظام';
@@ -623,7 +623,7 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get removeAccountConfirmation =>
- 'سيؤدي ذلك إلى حذف المهام والتقويمات والأحداث والتذكيرات والتغييرات غير المتصلة المعلّقة المخزّنة مؤقتًا من هذا الجهاز. ستُفقد التغييرات غير المتزامنة. لن يُحذف أي شيء من Google أو Microsoft.';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess =>
@@ -644,7 +644,22 @@ class AppLocalizationsAr extends AppLocalizations {
'تمت إزالة الحساب من هذا الجهاز، لكن تعذر على BusyMax إلغاء الوصول إلى Google. يمكنك إلغاء الوصول من حسابك على Google.';
@override
- String get newList => 'قائمة جديدة';
+ String get newTaskList => 'قائمة مهام جديدة';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
@override
String get signInToViewTaskLists => 'سجّل الدخول لعرض قوائم المهام.';
@@ -667,6 +682,17 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get deleteList => 'حذف القائمة';
+ @override
+ String get unshare => 'إلغاء المشاركة';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
@override
String get builtInMicrosoftList => 'مدمجة';
@@ -679,6 +705,16 @@ class AppLocalizationsAr extends AppLocalizations {
return 'حذف «$title» من Google Tasks؟';
}
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
@override
String get deleteEvent => 'حذف الحدث';
@@ -800,6 +836,74 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get doneStatus => 'منجزة';
+ @override
+ String get taskStatus => 'Status';
+
+ @override
+ String get taskStatusNone => 'No status';
+
+ @override
+ String get taskStatusNeedsAction => 'تحتاج إلى إجراء';
+
+ @override
+ String get taskStatusInProcess => 'تحت الإجراء';
+
+ @override
+ String get taskStatusCompleted => 'مُكتمِل';
+
+ @override
+ String get taskStatusCancelled => 'Cancelled';
+
+ @override
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
+
+ @override
+ String get completionDate => 'Completion date';
+
+ @override
+ String get priority => 'الأولوية';
+
+ @override
+ String get priorityNone => 'No priority';
+
+ @override
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
+
+ @override
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
+
+ @override
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
+
+ @override
+ String get taskUrl => 'URL';
+
+ @override
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
+
+ @override
+ String get classification => 'Classification';
+
+ @override
+ String get classificationPublic => 'When shared, show the full task';
+
+ @override
+ String get classificationConfidential => 'When shared, show only busy';
+
+ @override
+ String get classificationPrivate => 'When shared, hide this task';
+
+ @override
+ String get pinTask => 'Pin task';
+
@override
String get notes => 'ملاحظات';
@@ -836,6 +940,84 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get addReminder => 'إضافة تذكير';
+ @override
+ String get reminders => 'Reminders';
+
+ @override
+ String get noReminders => 'لا توجد أي تذكيرات';
+
+ @override
+ String get editReminder => 'Edit reminder';
+
+ @override
+ String get beforeTaskStarts => 'قبل أن تبدأ المهمة';
+
+ @override
+ String get beforeTaskDue => 'قبل اكتمال المهمة';
+
+ @override
+ String get afterTaskStarts => 'After the task starts';
+
+ @override
+ String get afterTaskDue => 'After the task is due';
+
+ @override
+ String get relativeToTaskStart => 'Relative to the task start date';
+
+ @override
+ String get relativeToTaskDue => 'Relative to the task due date';
+
+ @override
+ String get reminderTimeOfDay => 'Time of day';
+
+ @override
+ String get absoluteReminder => 'At a date and time';
+
+ @override
+ String get reminderAmount => 'Amount';
+
+ @override
+ String get reminderUnit => 'Unit';
+
+ @override
+ String get reminderUnitSeconds => 'Seconds';
+
+ @override
+ String get reminderUnitMinutes => 'Minutes';
+
+ @override
+ String get reminderUnitHours => 'Hours';
+
+ @override
+ String get reminderUnitDays => 'Days';
+
+ @override
+ String get reminderUnitWeeks => 'Weeks';
+
+ @override
+ String get reminderAtTaskStart => 'At the task start';
+
+ @override
+ String get reminderAtTaskDue => 'At the task due time';
+
+ @override
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
+
+ @override
+ String get relatedRemindersTitle => 'Keep related reminders?';
+
+ @override
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
+
+ @override
+ String get discardRelatedReminders => 'Discard reminders';
+
+ @override
+ String get keepRelatedReminders => 'Keep reminders';
+
@override
String get addGuest => 'إضافة مدعو';
@@ -869,6 +1051,123 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get repeatYearly => 'سنويًا';
+ @override
+ String get repeatEvery => 'تكرار كل';
+
+ @override
+ String get repeatOn => 'Repeat on';
+
+ @override
+ String get repeatEnd => 'نهاية التكرار';
+
+ @override
+ String get repeatNever => 'Never';
+
+ @override
+ String get repeatUntil => 'On date';
+
+ @override
+ String get repeatAfter => 'After a number of occurrences';
+
+ @override
+ String get repeatCount => 'Occurrences';
+
+ @override
+ String get repeatDayOfMonth => 'Days of month';
+
+ @override
+ String get repeatMonths => 'Months';
+
+ @override
+ String get repeatOrdinal => 'Weekday position';
+
+ @override
+ String get repeatSpecificDays => 'Specific days';
+
+ @override
+ String get repeatFirst => 'First';
+
+ @override
+ String get repeatSecond => 'Second';
+
+ @override
+ String get repeatThird => 'Third';
+
+ @override
+ String get repeatFourth => 'Fourth';
+
+ @override
+ String get repeatFifth => 'Fifth';
+
+ @override
+ String get repeatSecondToLast => 'Second to last';
+
+ @override
+ String get repeatLast => 'Last';
+
+ @override
+ String get repeatAnyDay => 'Day';
+
+ @override
+ String get repeatWeekday => 'Weekday';
+
+ @override
+ String get repeatWeekendDay => 'Weekend day';
+
+ @override
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
+
+ @override
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
+
+ @override
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
+
+ @override
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
+
+ @override
+ String repeatOnDaysSummary(String days) {
+ return 'on $days';
+ }
+
+ @override
+ String repeatOnMonthDaysSummary(String days) {
+ return 'في يوم $days';
+ }
+
+ @override
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
+
+ @override
+ String repeatInMonthsSummary(String months) {
+ return 'in $months';
+ }
+
+ @override
+ String repeatTimesSummary(int count) {
+ return '$count مرات';
+ }
+
+ @override
+ String repeatUntilSummary(String date) {
+ return 'until $date';
+ }
+
+ @override
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
+
@override
String get importance => 'الأهمية';
@@ -918,6 +1217,26 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get createSubtask => 'إنشاء مهمة فرعية';
+ @override
+ String get subtasks => 'مهام فرعية';
+
+ @override
+ String get duplicateTask => 'Duplicate task';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => 'إخْفِ المهام الفرعية';
+
+ @override
+ String get hideClosedSubtasks => 'إخف المهام الفرعية المغلقة';
+
@override
String get moveToTop => 'نقل إلى الأعلى';
@@ -929,7 +1248,7 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String deleteTaskConfirmation(String title) {
- return 'حذف «$title» من Google Tasks؟';
+ return 'حذف «$title»؟';
}
@override
@@ -1256,6 +1575,223 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get noLocationsFound => 'لم يتم العثور على مواقع';
+ @override
+ String get requiredField => 'This field is required.';
+
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
+
+ @override
+ String get appleICloudProvider => 'Apple iCloud Calendar';
+
+ @override
+ String get nextcloudProvider => 'Nextcloud';
+
+ @override
+ String get appleICloudTasksProvider => 'Apple iCloud';
+
+ @override
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
+
+ @override
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
+
+ @override
+ String get addNextcloudAccount => 'Add Nextcloud account';
+
+ @override
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
+
+ @override
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
+
+ @override
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
+
+ @override
+ String get appleAccountEmail => 'Apple Account email';
+
+ @override
+ String get appleAppSpecificPassword => 'App-specific password';
+
+ @override
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
+
+ @override
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
+
+ @override
+ String get connectNextcloudTitle => 'Connect Nextcloud';
+
+ @override
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
+
+ @override
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
+
+ @override
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
+
+ @override
+ String get connectAccountAction => 'Connect';
+
+ @override
+ String get cancelAccountConnection => 'Cancel connection';
+
+ @override
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
+
+ @override
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
+
+ @override
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
+
+ @override
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
+
+ @override
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
+
+ @override
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
+
+ @override
+ String get collectionSettings => 'Calendars and task lists';
+
+ @override
+ String get calendarContent => 'Calendar events';
+
+ @override
+ String get taskContent => 'Tasks';
+
+ @override
+ String get readOnlySharedCollection => 'Read-only';
+
+ @override
+ String get pendingLocally => 'Pending locally';
+
+ @override
+ String get conflictBlocked => 'Blocked by conflict';
+
+ @override
+ String get authenticationBlocked => 'Blocked until reconnect';
+
+ @override
+ String get operationFailed => 'Operation failed';
+
+ @override
+ String get keepServerVersion => 'Keep server version';
+
+ @override
+ String get reapplyLocalChange => 'Review and reapply local change';
+
+ @override
+ String get duplicateLocalItem => 'Duplicate as new item';
+
+ @override
+ String get davConnectionState => 'Connection state';
+
+ @override
+ String get davConnected => 'Connected';
+
+ @override
+ String get davConnecting => 'Connecting…';
+
+ @override
+ String get davSignedOut => 'Signed out';
+
+ @override
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
+
+ @override
+ String get thisAndFutureUnavailable => 'This and future (not available)';
+
+ @override
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
+
+ @override
+ String get taskDueBeforeStart => 'يجب ألا يكون موعد الاستحقاق قبل وقت البدء.';
+
+ @override
+ String get taskStartDueTimeModeMismatch =>
+ 'عيّن وقتًا لكل من البدء والاستحقاق، أو اجعل المهمة طوال اليوم.';
+
@override
String deleteCalendarConfirmation(String title) {
return 'حذف «$title»؟';
diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart
index 0060526..b2bc599 100644
--- a/lib/l10n/generated/app_localizations_de.dart
+++ b/lib/l10n/generated/app_localizations_de.dart
@@ -17,7 +17,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get connectGoogleAccount =>
- 'Verbinden Sie Google- und Microsoft-Konten, um Kalender und Aufgaben zu synchronisieren.';
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
@override
String get googlePermissionsConsentNotice =>
@@ -41,7 +41,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get onboardingAccountsStepDescription =>
- 'Fügen Sie alle Google- und Microsoft-Konten hinzu, die Sie verwenden möchten. BusyMax synchronisiert Kalender, Termine, Aufgabenlisten und Aufgaben aus jedem Konto.';
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
@override
String get onboardingPreferencesStepTitle => 'Systemeinstellungen wählen';
@@ -613,7 +613,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get removeAccountConfirmation =>
- 'Dadurch werden zwischengespeicherte Aufgaben, Kalender, Termine, Erinnerungen und ausstehende Offline-Änderungen von diesem Gerät gelöscht. Nicht synchronisierte Änderungen gehen verloren. Bei Google oder Microsoft wird nichts gelöscht.';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess =>
@@ -635,7 +635,22 @@ class AppLocalizationsDe extends AppLocalizations {
'Das Konto wurde von diesem Gerät entfernt, aber BusyMax konnte den Google-Zugriff nicht widerrufen. Sie können ihn in Ihrem Google-Konto widerrufen.';
@override
- String get newList => 'Neue Liste';
+ String get newTaskList => 'Neue Aufgabenliste';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
@override
String get signInToViewTaskLists =>
@@ -659,6 +674,17 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get deleteList => 'Liste löschen';
+ @override
+ String get unshare => 'Freigabe aufheben';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
@override
String get builtInMicrosoftList => 'Integriert';
@@ -671,6 +697,16 @@ class AppLocalizationsDe extends AppLocalizations {
return '\"$title\" aus Google Tasks löschen?';
}
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
@override
String get deleteEvent => 'Termin löschen';
@@ -796,6 +832,74 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get doneStatus => 'Erledigt';
+ @override
+ String get taskStatus => 'Status';
+
+ @override
+ String get taskStatusNone => 'No status';
+
+ @override
+ String get taskStatusNeedsAction => 'Handlungsbedarf';
+
+ @override
+ String get taskStatusInProcess => 'In Bearbeitung';
+
+ @override
+ String get taskStatusCompleted => 'Fertiggestellt';
+
+ @override
+ String get taskStatusCancelled => 'Cancelled';
+
+ @override
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
+
+ @override
+ String get completionDate => 'Completion date';
+
+ @override
+ String get priority => 'Priorität';
+
+ @override
+ String get priorityNone => 'No priority';
+
+ @override
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
+
+ @override
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
+
+ @override
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
+
+ @override
+ String get taskUrl => 'URL';
+
+ @override
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
+
+ @override
+ String get classification => 'Classification';
+
+ @override
+ String get classificationPublic => 'When shared, show the full task';
+
+ @override
+ String get classificationConfidential => 'When shared, show only busy';
+
+ @override
+ String get classificationPrivate => 'When shared, hide this task';
+
+ @override
+ String get pinTask => 'Pin task';
+
@override
String get notes => 'Notizen';
@@ -832,6 +936,84 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get addReminder => 'Erinnerung hinzufügen';
+ @override
+ String get reminders => 'Reminders';
+
+ @override
+ String get noReminders => 'Keine Erinnerungen';
+
+ @override
+ String get editReminder => 'Edit reminder';
+
+ @override
+ String get beforeTaskStarts => 'Bevor die Aufgabe startet';
+
+ @override
+ String get beforeTaskDue => 'Bevor die Aufgabe fällig ist';
+
+ @override
+ String get afterTaskStarts => 'After the task starts';
+
+ @override
+ String get afterTaskDue => 'After the task is due';
+
+ @override
+ String get relativeToTaskStart => 'Relative to the task start date';
+
+ @override
+ String get relativeToTaskDue => 'Relative to the task due date';
+
+ @override
+ String get reminderTimeOfDay => 'Time of day';
+
+ @override
+ String get absoluteReminder => 'At a date and time';
+
+ @override
+ String get reminderAmount => 'Amount';
+
+ @override
+ String get reminderUnit => 'Unit';
+
+ @override
+ String get reminderUnitSeconds => 'Seconds';
+
+ @override
+ String get reminderUnitMinutes => 'Minutes';
+
+ @override
+ String get reminderUnitHours => 'Hours';
+
+ @override
+ String get reminderUnitDays => 'Days';
+
+ @override
+ String get reminderUnitWeeks => 'Weeks';
+
+ @override
+ String get reminderAtTaskStart => 'At the task start';
+
+ @override
+ String get reminderAtTaskDue => 'At the task due time';
+
+ @override
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
+
+ @override
+ String get relatedRemindersTitle => 'Keep related reminders?';
+
+ @override
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
+
+ @override
+ String get discardRelatedReminders => 'Erinnerungen verwerfen';
+
+ @override
+ String get keepRelatedReminders => 'Erinnerungen behalten';
+
@override
String get addGuest => 'Gast hinzufügen';
@@ -865,6 +1047,123 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeatYearly => 'Jährlich';
+ @override
+ String get repeatEvery => 'Wiederhole jeden';
+
+ @override
+ String get repeatOn => 'Repeat on';
+
+ @override
+ String get repeatEnd => 'Wiederholung beenden';
+
+ @override
+ String get repeatNever => 'Never';
+
+ @override
+ String get repeatUntil => 'On date';
+
+ @override
+ String get repeatAfter => 'After a number of occurrences';
+
+ @override
+ String get repeatCount => 'Occurrences';
+
+ @override
+ String get repeatDayOfMonth => 'Days of month';
+
+ @override
+ String get repeatMonths => 'Months';
+
+ @override
+ String get repeatOrdinal => 'Weekday position';
+
+ @override
+ String get repeatSpecificDays => 'Specific days';
+
+ @override
+ String get repeatFirst => 'First';
+
+ @override
+ String get repeatSecond => 'Second';
+
+ @override
+ String get repeatThird => 'Third';
+
+ @override
+ String get repeatFourth => 'Fourth';
+
+ @override
+ String get repeatFifth => 'Fifth';
+
+ @override
+ String get repeatSecondToLast => 'Second to last';
+
+ @override
+ String get repeatLast => 'Last';
+
+ @override
+ String get repeatAnyDay => 'Day';
+
+ @override
+ String get repeatWeekday => 'Weekday';
+
+ @override
+ String get repeatWeekendDay => 'Weekend day';
+
+ @override
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
+
+ @override
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
+
+ @override
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
+
+ @override
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
+
+ @override
+ String repeatOnDaysSummary(String days) {
+ return 'an $days';
+ }
+
+ @override
+ String repeatOnMonthDaysSummary(String days) {
+ return 'am Tag $days';
+ }
+
+ @override
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
+
+ @override
+ String repeatInMonthsSummary(String months) {
+ return 'in $months';
+ }
+
+ @override
+ String repeatTimesSummary(int count) {
+ return '$count mal';
+ }
+
+ @override
+ String repeatUntilSummary(String date) {
+ return 'bis $date';
+ }
+
+ @override
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
+
@override
String get importance => 'Wichtigkeit';
@@ -914,6 +1213,26 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get createSubtask => 'Unteraufgabe erstellen';
+ @override
+ String get subtasks => 'Unteraufgaben';
+
+ @override
+ String get duplicateTask => 'Aufgabe duplizieren';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => 'Teilaufgaben ausblenden';
+
+ @override
+ String get hideClosedSubtasks => 'Geschlossene Teilaufgaben ausblenden';
+
@override
String get moveToTop => 'Ganz nach oben verschieben';
@@ -925,7 +1244,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String deleteTaskConfirmation(String title) {
- return '\"$title\" aus Google Tasks löschen?';
+ return '\"$title\" löschen?';
}
@override
@@ -1253,6 +1572,224 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get noLocationsFound => 'Keine Orte gefunden';
+ @override
+ String get requiredField => 'This field is required.';
+
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
+
+ @override
+ String get appleICloudProvider => 'Apple iCloud Calendar';
+
+ @override
+ String get nextcloudProvider => 'Nextcloud';
+
+ @override
+ String get appleICloudTasksProvider => 'Apple iCloud';
+
+ @override
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
+
+ @override
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
+
+ @override
+ String get addNextcloudAccount => 'Add Nextcloud account';
+
+ @override
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
+
+ @override
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
+
+ @override
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
+
+ @override
+ String get appleAccountEmail => 'Apple Account email';
+
+ @override
+ String get appleAppSpecificPassword => 'App-specific password';
+
+ @override
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
+
+ @override
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
+
+ @override
+ String get connectNextcloudTitle => 'Connect Nextcloud';
+
+ @override
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
+
+ @override
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
+
+ @override
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
+
+ @override
+ String get connectAccountAction => 'Connect';
+
+ @override
+ String get cancelAccountConnection => 'Cancel connection';
+
+ @override
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
+
+ @override
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
+
+ @override
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
+
+ @override
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
+
+ @override
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
+
+ @override
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
+
+ @override
+ String get collectionSettings => 'Calendars and task lists';
+
+ @override
+ String get calendarContent => 'Calendar events';
+
+ @override
+ String get taskContent => 'Tasks';
+
+ @override
+ String get readOnlySharedCollection => 'Read-only';
+
+ @override
+ String get pendingLocally => 'Pending locally';
+
+ @override
+ String get conflictBlocked => 'Blocked by conflict';
+
+ @override
+ String get authenticationBlocked => 'Blocked until reconnect';
+
+ @override
+ String get operationFailed => 'Operation failed';
+
+ @override
+ String get keepServerVersion => 'Keep server version';
+
+ @override
+ String get reapplyLocalChange => 'Review and reapply local change';
+
+ @override
+ String get duplicateLocalItem => 'Duplicate as new item';
+
+ @override
+ String get davConnectionState => 'Connection state';
+
+ @override
+ String get davConnected => 'Connected';
+
+ @override
+ String get davConnecting => 'Connecting…';
+
+ @override
+ String get davSignedOut => 'Signed out';
+
+ @override
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
+
+ @override
+ String get thisAndFutureUnavailable => 'This and future (not available)';
+
+ @override
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
+
+ @override
+ String get taskDueBeforeStart =>
+ 'Die Fälligkeit darf nicht vor dem Beginn liegen.';
+
+ @override
+ String get taskStartDueTimeModeMismatch =>
+ 'Lege für Beginn und Fälligkeit jeweils eine Uhrzeit fest oder mache die Aufgabe ganztägig.';
+
@override
String deleteCalendarConfirmation(String title) {
return '\"$title\" löschen?';
diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart
index e858e70..f5d7745 100644
--- a/lib/l10n/generated/app_localizations_en.dart
+++ b/lib/l10n/generated/app_localizations_en.dart
@@ -17,7 +17,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get connectGoogleAccount =>
- 'Connect Google and Microsoft accounts to sync calendars and tasks.';
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
@override
String get googlePermissionsConsentNotice =>
@@ -41,7 +41,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get onboardingAccountsStepDescription =>
- 'Add all Google and Microsoft accounts you want to use. BusyMax syncs calendars, events, task lists, and tasks from each account.';
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
@override
String get onboardingPreferencesStepTitle => 'Choose system settings';
@@ -609,7 +609,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get removeAccountConfirmation =>
- 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Nothing will be deleted from Google or Microsoft.';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess =>
@@ -631,7 +631,22 @@ class AppLocalizationsEn extends AppLocalizations {
'The account was removed from this device, but BusyMax could not revoke Google access. You can revoke it from your Google Account.';
@override
- String get newList => 'New list';
+ String get newTaskList => 'New task list';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
@override
String get signInToViewTaskLists => 'Sign in to view task lists.';
@@ -654,6 +669,17 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get deleteList => 'Delete list';
+ @override
+ String get unshare => 'Unshare';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
@override
String get builtInMicrosoftList => 'Built-in';
@@ -666,6 +692,16 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Delete \"$title\" from Google Tasks?';
}
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
@override
String get deleteEvent => 'Delete Event';
@@ -788,6 +824,74 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get doneStatus => 'Done';
+ @override
+ String get taskStatus => 'Status';
+
+ @override
+ String get taskStatusNone => 'No status';
+
+ @override
+ String get taskStatusNeedsAction => 'Needs action';
+
+ @override
+ String get taskStatusInProcess => 'In process';
+
+ @override
+ String get taskStatusCompleted => 'Completed';
+
+ @override
+ String get taskStatusCancelled => 'Cancelled';
+
+ @override
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
+
+ @override
+ String get completionDate => 'Completion date';
+
+ @override
+ String get priority => 'Priority';
+
+ @override
+ String get priorityNone => 'No priority';
+
+ @override
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
+
+ @override
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
+
+ @override
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
+
+ @override
+ String get taskUrl => 'URL';
+
+ @override
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
+
+ @override
+ String get classification => 'Classification';
+
+ @override
+ String get classificationPublic => 'When shared, show the full task';
+
+ @override
+ String get classificationConfidential => 'When shared, show only busy';
+
+ @override
+ String get classificationPrivate => 'When shared, hide this task';
+
+ @override
+ String get pinTask => 'Pin task';
+
@override
String get notes => 'Notes';
@@ -824,6 +928,84 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get addReminder => 'Add Reminder';
+ @override
+ String get reminders => 'Reminders';
+
+ @override
+ String get noReminders => 'No reminders';
+
+ @override
+ String get editReminder => 'Edit reminder';
+
+ @override
+ String get beforeTaskStarts => 'Before the task starts';
+
+ @override
+ String get beforeTaskDue => 'Before the task is due';
+
+ @override
+ String get afterTaskStarts => 'After the task starts';
+
+ @override
+ String get afterTaskDue => 'After the task is due';
+
+ @override
+ String get relativeToTaskStart => 'Relative to the task start date';
+
+ @override
+ String get relativeToTaskDue => 'Relative to the task due date';
+
+ @override
+ String get reminderTimeOfDay => 'Time of day';
+
+ @override
+ String get absoluteReminder => 'At a date and time';
+
+ @override
+ String get reminderAmount => 'Amount';
+
+ @override
+ String get reminderUnit => 'Unit';
+
+ @override
+ String get reminderUnitSeconds => 'Seconds';
+
+ @override
+ String get reminderUnitMinutes => 'Minutes';
+
+ @override
+ String get reminderUnitHours => 'Hours';
+
+ @override
+ String get reminderUnitDays => 'Days';
+
+ @override
+ String get reminderUnitWeeks => 'Weeks';
+
+ @override
+ String get reminderAtTaskStart => 'At the task start';
+
+ @override
+ String get reminderAtTaskDue => 'At the task due time';
+
+ @override
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
+
+ @override
+ String get relatedRemindersTitle => 'Keep related reminders?';
+
+ @override
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
+
+ @override
+ String get discardRelatedReminders => 'Discard reminders';
+
+ @override
+ String get keepRelatedReminders => 'Keep reminders';
+
@override
String get addGuest => 'Add Guest';
@@ -857,6 +1039,123 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get repeatYearly => 'Yearly';
+ @override
+ String get repeatEvery => 'Repeat every';
+
+ @override
+ String get repeatOn => 'Repeat on';
+
+ @override
+ String get repeatEnd => 'End repeat';
+
+ @override
+ String get repeatNever => 'Never';
+
+ @override
+ String get repeatUntil => 'On date';
+
+ @override
+ String get repeatAfter => 'After a number of occurrences';
+
+ @override
+ String get repeatCount => 'Occurrences';
+
+ @override
+ String get repeatDayOfMonth => 'Days of month';
+
+ @override
+ String get repeatMonths => 'Months';
+
+ @override
+ String get repeatOrdinal => 'Weekday position';
+
+ @override
+ String get repeatSpecificDays => 'Specific days';
+
+ @override
+ String get repeatFirst => 'First';
+
+ @override
+ String get repeatSecond => 'Second';
+
+ @override
+ String get repeatThird => 'Third';
+
+ @override
+ String get repeatFourth => 'Fourth';
+
+ @override
+ String get repeatFifth => 'Fifth';
+
+ @override
+ String get repeatSecondToLast => 'Second to last';
+
+ @override
+ String get repeatLast => 'Last';
+
+ @override
+ String get repeatAnyDay => 'Day';
+
+ @override
+ String get repeatWeekday => 'Weekday';
+
+ @override
+ String get repeatWeekendDay => 'Weekend day';
+
+ @override
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
+
+ @override
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
+
+ @override
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
+
+ @override
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
+
+ @override
+ String repeatOnDaysSummary(String days) {
+ return 'on $days';
+ }
+
+ @override
+ String repeatOnMonthDaysSummary(String days) {
+ return 'on day $days';
+ }
+
+ @override
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
+
+ @override
+ String repeatInMonthsSummary(String months) {
+ return 'in $months';
+ }
+
+ @override
+ String repeatTimesSummary(int count) {
+ return '$count times';
+ }
+
+ @override
+ String repeatUntilSummary(String date) {
+ return 'until $date';
+ }
+
+ @override
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
+
@override
String get importance => 'Importance';
@@ -906,6 +1205,26 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get createSubtask => 'Create subtask';
+ @override
+ String get subtasks => 'Subtasks';
+
+ @override
+ String get duplicateTask => 'Duplicate task';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => 'Hide subtasks';
+
+ @override
+ String get hideClosedSubtasks => 'Hide closed subtasks';
+
@override
String get moveToTop => 'Move to top';
@@ -917,7 +1236,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String deleteTaskConfirmation(String title) {
- return 'Delete \"$title\" from Google Tasks?';
+ return 'Delete \"$title\"?';
}
@override
@@ -1237,6 +1556,223 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get noLocationsFound => 'No locations found';
+ @override
+ String get requiredField => 'This field is required.';
+
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
+
+ @override
+ String get appleICloudProvider => 'Apple iCloud Calendar';
+
+ @override
+ String get nextcloudProvider => 'Nextcloud';
+
+ @override
+ String get appleICloudTasksProvider => 'Apple iCloud';
+
+ @override
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
+
+ @override
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
+
+ @override
+ String get addNextcloudAccount => 'Add Nextcloud account';
+
+ @override
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
+
+ @override
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
+
+ @override
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
+
+ @override
+ String get appleAccountEmail => 'Apple Account email';
+
+ @override
+ String get appleAppSpecificPassword => 'App-specific password';
+
+ @override
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
+
+ @override
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
+
+ @override
+ String get connectNextcloudTitle => 'Connect Nextcloud';
+
+ @override
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
+
+ @override
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
+
+ @override
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
+
+ @override
+ String get connectAccountAction => 'Connect';
+
+ @override
+ String get cancelAccountConnection => 'Cancel connection';
+
+ @override
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
+
+ @override
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
+
+ @override
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
+
+ @override
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
+
+ @override
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
+
+ @override
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
+
+ @override
+ String get collectionSettings => 'Calendars and task lists';
+
+ @override
+ String get calendarContent => 'Calendar events';
+
+ @override
+ String get taskContent => 'Tasks';
+
+ @override
+ String get readOnlySharedCollection => 'Read-only';
+
+ @override
+ String get pendingLocally => 'Pending locally';
+
+ @override
+ String get conflictBlocked => 'Blocked by conflict';
+
+ @override
+ String get authenticationBlocked => 'Blocked until reconnect';
+
+ @override
+ String get operationFailed => 'Operation failed';
+
+ @override
+ String get keepServerVersion => 'Keep server version';
+
+ @override
+ String get reapplyLocalChange => 'Review and reapply local change';
+
+ @override
+ String get duplicateLocalItem => 'Duplicate as new item';
+
+ @override
+ String get davConnectionState => 'Connection state';
+
+ @override
+ String get davConnected => 'Connected';
+
+ @override
+ String get davConnecting => 'Connecting…';
+
+ @override
+ String get davSignedOut => 'Signed out';
+
+ @override
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
+
+ @override
+ String get thisAndFutureUnavailable => 'This and future (not available)';
+
+ @override
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
+
+ @override
+ String get taskDueBeforeStart => 'Due must not be before start.';
+
+ @override
+ String get taskStartDueTimeModeMismatch =>
+ 'Set times for both start and due, or make the task all day.';
+
@override
String deleteCalendarConfirmation(String title) {
return 'Delete \"$title\"?';
diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart
index 6a0da5f..2e88b64 100644
--- a/lib/l10n/generated/app_localizations_es.dart
+++ b/lib/l10n/generated/app_localizations_es.dart
@@ -17,7 +17,7 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get connectGoogleAccount =>
- 'Conecta cuentas de Google y Microsoft para sincronizar calendarios y tareas.';
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
@override
String get googlePermissionsConsentNotice =>
@@ -41,7 +41,7 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get onboardingAccountsStepDescription =>
- 'Añade todas las cuentas de Google y Microsoft que quieras usar. BusyMax sincroniza calendarios, eventos, listas de tareas y tareas de cada cuenta.';
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
@override
String get onboardingPreferencesStepTitle => 'Elegir ajustes del sistema';
@@ -615,7 +615,7 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get removeAccountConfirmation =>
- 'Esto elimina de este dispositivo las tareas, los calendarios, los eventos, los recordatorios y los cambios sin conexión pendientes almacenados en caché. Los cambios no sincronizados se perderán. No se eliminará nada de Google ni Microsoft.';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess =>
@@ -637,7 +637,22 @@ class AppLocalizationsEs extends AppLocalizations {
'La cuenta se eliminó de este dispositivo, pero BusyMax no pudo revocar su acceso a tu cuenta de Google. Puedes revocarlo desde tu cuenta de Google.';
@override
- String get newList => 'Nueva lista';
+ String get newTaskList => 'Nueva lista de tareas';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
@override
String get signInToViewTaskLists =>
@@ -661,6 +676,17 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get deleteList => 'Eliminar lista';
+ @override
+ String get unshare => 'No compartir';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
@override
String get builtInMicrosoftList => 'Integrada';
@@ -673,6 +699,16 @@ class AppLocalizationsEs extends AppLocalizations {
return '¿Eliminar \"$title\" de Google Tasks?';
}
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
@override
String get deleteEvent => 'Eliminar evento';
@@ -797,6 +833,74 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get doneStatus => 'Completada';
+ @override
+ String get taskStatus => 'Status';
+
+ @override
+ String get taskStatusNone => 'No status';
+
+ @override
+ String get taskStatusNeedsAction => 'Necesita una acción';
+
+ @override
+ String get taskStatusInProcess => 'En proceso';
+
+ @override
+ String get taskStatusCompleted => 'Completada';
+
+ @override
+ String get taskStatusCancelled => 'Cancelled';
+
+ @override
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
+
+ @override
+ String get completionDate => 'Completion date';
+
+ @override
+ String get priority => 'Prioridad';
+
+ @override
+ String get priorityNone => 'No priority';
+
+ @override
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
+
+ @override
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
+
+ @override
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
+
+ @override
+ String get taskUrl => 'URL';
+
+ @override
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
+
+ @override
+ String get classification => 'Classification';
+
+ @override
+ String get classificationPublic => 'When shared, show the full task';
+
+ @override
+ String get classificationConfidential => 'When shared, show only busy';
+
+ @override
+ String get classificationPrivate => 'When shared, hide this task';
+
+ @override
+ String get pinTask => 'Pin task';
+
@override
String get notes => 'Notas';
@@ -833,6 +937,84 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get addReminder => 'Añadir recordatorio';
+ @override
+ String get reminders => 'Reminders';
+
+ @override
+ String get noReminders => 'Sin recordatorio';
+
+ @override
+ String get editReminder => 'Edit reminder';
+
+ @override
+ String get beforeTaskStarts => 'Antes de empezar la tarea';
+
+ @override
+ String get beforeTaskDue => 'Antes de terminar la tarea';
+
+ @override
+ String get afterTaskStarts => 'After the task starts';
+
+ @override
+ String get afterTaskDue => 'After the task is due';
+
+ @override
+ String get relativeToTaskStart => 'Relative to the task start date';
+
+ @override
+ String get relativeToTaskDue => 'Relative to the task due date';
+
+ @override
+ String get reminderTimeOfDay => 'Time of day';
+
+ @override
+ String get absoluteReminder => 'At a date and time';
+
+ @override
+ String get reminderAmount => 'Amount';
+
+ @override
+ String get reminderUnit => 'Unit';
+
+ @override
+ String get reminderUnitSeconds => 'Seconds';
+
+ @override
+ String get reminderUnitMinutes => 'Minutes';
+
+ @override
+ String get reminderUnitHours => 'Hours';
+
+ @override
+ String get reminderUnitDays => 'Days';
+
+ @override
+ String get reminderUnitWeeks => 'Weeks';
+
+ @override
+ String get reminderAtTaskStart => 'At the task start';
+
+ @override
+ String get reminderAtTaskDue => 'At the task due time';
+
+ @override
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
+
+ @override
+ String get relatedRemindersTitle => 'Keep related reminders?';
+
+ @override
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
+
+ @override
+ String get discardRelatedReminders => 'Descartar recordatorios';
+
+ @override
+ String get keepRelatedReminders => 'Mantener recordatorios';
+
@override
String get addGuest => 'Añadir invitado';
@@ -866,6 +1048,123 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get repeatYearly => 'Anual';
+ @override
+ String get repeatEvery => 'Repetir cada';
+
+ @override
+ String get repeatOn => 'Repeat on';
+
+ @override
+ String get repeatEnd => 'Finalizar repetición';
+
+ @override
+ String get repeatNever => 'Never';
+
+ @override
+ String get repeatUntil => 'On date';
+
+ @override
+ String get repeatAfter => 'After a number of occurrences';
+
+ @override
+ String get repeatCount => 'Occurrences';
+
+ @override
+ String get repeatDayOfMonth => 'Days of month';
+
+ @override
+ String get repeatMonths => 'Months';
+
+ @override
+ String get repeatOrdinal => 'Weekday position';
+
+ @override
+ String get repeatSpecificDays => 'Specific days';
+
+ @override
+ String get repeatFirst => 'First';
+
+ @override
+ String get repeatSecond => 'Second';
+
+ @override
+ String get repeatThird => 'Third';
+
+ @override
+ String get repeatFourth => 'Fourth';
+
+ @override
+ String get repeatFifth => 'Fifth';
+
+ @override
+ String get repeatSecondToLast => 'Second to last';
+
+ @override
+ String get repeatLast => 'Last';
+
+ @override
+ String get repeatAnyDay => 'Day';
+
+ @override
+ String get repeatWeekday => 'Weekday';
+
+ @override
+ String get repeatWeekendDay => 'Weekend day';
+
+ @override
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
+
+ @override
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
+
+ @override
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
+
+ @override
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
+
+ @override
+ String repeatOnDaysSummary(String days) {
+ return 'dentro de $days';
+ }
+
+ @override
+ String repeatOnMonthDaysSummary(String days) {
+ return 'en día $days';
+ }
+
+ @override
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
+
+ @override
+ String repeatInMonthsSummary(String months) {
+ return 'en $months';
+ }
+
+ @override
+ String repeatTimesSummary(int count) {
+ return '$count veces';
+ }
+
+ @override
+ String repeatUntilSummary(String date) {
+ return 'hasta el $date';
+ }
+
+ @override
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
+
@override
String get importance => 'Importancia';
@@ -915,6 +1214,26 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get createSubtask => 'Crear subtarea';
+ @override
+ String get subtasks => 'Subtareas';
+
+ @override
+ String get duplicateTask => 'Duplicar tarea';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => 'Ocultar subtareas';
+
+ @override
+ String get hideClosedSubtasks => 'Ocultar subtareas cerradas';
+
@override
String get moveToTop => 'Mover al principio';
@@ -926,7 +1245,7 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String deleteTaskConfirmation(String title) {
- return '¿Eliminar \"$title\" de Google Tasks?';
+ return '¿Eliminar \"$title\"?';
}
@override
@@ -1254,6 +1573,224 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get noLocationsFound => 'No se encontraron ubicaciones';
+ @override
+ String get requiredField => 'This field is required.';
+
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
+
+ @override
+ String get appleICloudProvider => 'Apple iCloud Calendar';
+
+ @override
+ String get nextcloudProvider => 'Nextcloud';
+
+ @override
+ String get appleICloudTasksProvider => 'Apple iCloud';
+
+ @override
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
+
+ @override
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
+
+ @override
+ String get addNextcloudAccount => 'Add Nextcloud account';
+
+ @override
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
+
+ @override
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
+
+ @override
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
+
+ @override
+ String get appleAccountEmail => 'Apple Account email';
+
+ @override
+ String get appleAppSpecificPassword => 'App-specific password';
+
+ @override
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
+
+ @override
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
+
+ @override
+ String get connectNextcloudTitle => 'Connect Nextcloud';
+
+ @override
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
+
+ @override
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
+
+ @override
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
+
+ @override
+ String get connectAccountAction => 'Connect';
+
+ @override
+ String get cancelAccountConnection => 'Cancel connection';
+
+ @override
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
+
+ @override
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
+
+ @override
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
+
+ @override
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
+
+ @override
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
+
+ @override
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
+
+ @override
+ String get collectionSettings => 'Calendars and task lists';
+
+ @override
+ String get calendarContent => 'Calendar events';
+
+ @override
+ String get taskContent => 'Tasks';
+
+ @override
+ String get readOnlySharedCollection => 'Read-only';
+
+ @override
+ String get pendingLocally => 'Pending locally';
+
+ @override
+ String get conflictBlocked => 'Blocked by conflict';
+
+ @override
+ String get authenticationBlocked => 'Blocked until reconnect';
+
+ @override
+ String get operationFailed => 'Operation failed';
+
+ @override
+ String get keepServerVersion => 'Keep server version';
+
+ @override
+ String get reapplyLocalChange => 'Review and reapply local change';
+
+ @override
+ String get duplicateLocalItem => 'Duplicate as new item';
+
+ @override
+ String get davConnectionState => 'Connection state';
+
+ @override
+ String get davConnected => 'Connected';
+
+ @override
+ String get davConnecting => 'Connecting…';
+
+ @override
+ String get davSignedOut => 'Signed out';
+
+ @override
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
+
+ @override
+ String get thisAndFutureUnavailable => 'This and future (not available)';
+
+ @override
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
+
+ @override
+ String get taskDueBeforeStart =>
+ 'El vencimiento no puede ser anterior al inicio.';
+
+ @override
+ String get taskStartDueTimeModeMismatch =>
+ 'Define una hora tanto para el inicio como para el vencimiento, o configura la tarea para todo el día.';
+
@override
String deleteCalendarConfirmation(String title) {
return '¿Eliminar \"$title\"?';
diff --git a/lib/l10n/generated/app_localizations_et.dart b/lib/l10n/generated/app_localizations_et.dart
index cba33c2..06e218b 100644
--- a/lib/l10n/generated/app_localizations_et.dart
+++ b/lib/l10n/generated/app_localizations_et.dart
@@ -17,7 +17,7 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get connectGoogleAccount =>
- 'Ühendage Google\'i ja Microsofti kontod kalendrite ja ülesannete sünkroonimiseks.';
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
@override
String get googlePermissionsConsentNotice =>
@@ -41,7 +41,7 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get onboardingAccountsStepDescription =>
- 'Lisage kõik Google\'i ja Microsofti kontod, mida soovite kasutada. BusyMax sünkroonib iga konto kalendrid, sündmused, ülesandeloendid ja ülesanded.';
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
@override
String get onboardingPreferencesStepTitle => 'Süsteemiseadete valimine';
@@ -611,7 +611,7 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get removeAccountConfirmation =>
- 'See kustutab seadmest vahemällu salvestatud ülesanded, kalendrid, sündmused, meeldetuletused ja sünkroonimist ootavad võrguühenduseta muudatused. Sünkroonimata muudatused lähevad kaotsi. Google\'ist ega Microsoftist midagi ei kustutata.';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess =>
@@ -633,7 +633,22 @@ class AppLocalizationsEt extends AppLocalizations {
'Konto eemaldati sellest seadmest, kuid BusyMaxi juurdepääsu teie Google\'i kontole ei saanud tühistada. Saate selle oma Google\'i kontol käsitsi tühistada.';
@override
- String get newList => 'Uus loend';
+ String get newTaskList => 'Uus ülesandeloend';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
@override
String get signInToViewTaskLists =>
@@ -658,6 +673,17 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get deleteList => 'Kustuta loend';
+ @override
+ String get unshare => 'Unshare';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
@override
String get builtInMicrosoftList => 'Sisseehitatud';
@@ -670,6 +696,16 @@ class AppLocalizationsEt extends AppLocalizations {
return 'Kas kustutada „$title” Google Tasksist?';
}
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
@override
String get deleteEvent => 'Kustuta sündmus';
@@ -794,6 +830,74 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get doneStatus => 'Valmis';
+ @override
+ String get taskStatus => 'Status';
+
+ @override
+ String get taskStatusNone => 'No status';
+
+ @override
+ String get taskStatusNeedsAction => 'Needs action';
+
+ @override
+ String get taskStatusInProcess => 'In process';
+
+ @override
+ String get taskStatusCompleted => 'Completed';
+
+ @override
+ String get taskStatusCancelled => 'Cancelled';
+
+ @override
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
+
+ @override
+ String get completionDate => 'Completion date';
+
+ @override
+ String get priority => 'Priority';
+
+ @override
+ String get priorityNone => 'No priority';
+
+ @override
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
+
+ @override
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
+
+ @override
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
+
+ @override
+ String get taskUrl => 'URL';
+
+ @override
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
+
+ @override
+ String get classification => 'Classification';
+
+ @override
+ String get classificationPublic => 'When shared, show the full task';
+
+ @override
+ String get classificationConfidential => 'When shared, show only busy';
+
+ @override
+ String get classificationPrivate => 'When shared, hide this task';
+
+ @override
+ String get pinTask => 'Pin task';
+
@override
String get notes => 'Märkmed';
@@ -830,6 +934,84 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get addReminder => 'Lisa meeldetuletus';
+ @override
+ String get reminders => 'Reminders';
+
+ @override
+ String get noReminders => 'No reminders';
+
+ @override
+ String get editReminder => 'Edit reminder';
+
+ @override
+ String get beforeTaskStarts => 'Before the task starts';
+
+ @override
+ String get beforeTaskDue => 'Before the task is due';
+
+ @override
+ String get afterTaskStarts => 'After the task starts';
+
+ @override
+ String get afterTaskDue => 'After the task is due';
+
+ @override
+ String get relativeToTaskStart => 'Relative to the task start date';
+
+ @override
+ String get relativeToTaskDue => 'Relative to the task due date';
+
+ @override
+ String get reminderTimeOfDay => 'Time of day';
+
+ @override
+ String get absoluteReminder => 'At a date and time';
+
+ @override
+ String get reminderAmount => 'Amount';
+
+ @override
+ String get reminderUnit => 'Unit';
+
+ @override
+ String get reminderUnitSeconds => 'Seconds';
+
+ @override
+ String get reminderUnitMinutes => 'Minutes';
+
+ @override
+ String get reminderUnitHours => 'Hours';
+
+ @override
+ String get reminderUnitDays => 'Days';
+
+ @override
+ String get reminderUnitWeeks => 'Weeks';
+
+ @override
+ String get reminderAtTaskStart => 'At the task start';
+
+ @override
+ String get reminderAtTaskDue => 'At the task due time';
+
+ @override
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
+
+ @override
+ String get relatedRemindersTitle => 'Keep related reminders?';
+
+ @override
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
+
+ @override
+ String get discardRelatedReminders => 'Discard reminders';
+
+ @override
+ String get keepRelatedReminders => 'Keep reminders';
+
@override
String get addGuest => 'Lisa külaline';
@@ -863,6 +1045,123 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get repeatYearly => 'Iga aasta';
+ @override
+ String get repeatEvery => 'Repeat every';
+
+ @override
+ String get repeatOn => 'Repeat on';
+
+ @override
+ String get repeatEnd => 'End repeat';
+
+ @override
+ String get repeatNever => 'Never';
+
+ @override
+ String get repeatUntil => 'On date';
+
+ @override
+ String get repeatAfter => 'After a number of occurrences';
+
+ @override
+ String get repeatCount => 'Occurrences';
+
+ @override
+ String get repeatDayOfMonth => 'Days of month';
+
+ @override
+ String get repeatMonths => 'Months';
+
+ @override
+ String get repeatOrdinal => 'Weekday position';
+
+ @override
+ String get repeatSpecificDays => 'Specific days';
+
+ @override
+ String get repeatFirst => 'First';
+
+ @override
+ String get repeatSecond => 'Second';
+
+ @override
+ String get repeatThird => 'Third';
+
+ @override
+ String get repeatFourth => 'Fourth';
+
+ @override
+ String get repeatFifth => 'Fifth';
+
+ @override
+ String get repeatSecondToLast => 'Second to last';
+
+ @override
+ String get repeatLast => 'Last';
+
+ @override
+ String get repeatAnyDay => 'Day';
+
+ @override
+ String get repeatWeekday => 'Weekday';
+
+ @override
+ String get repeatWeekendDay => 'Weekend day';
+
+ @override
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
+
+ @override
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
+
+ @override
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
+
+ @override
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
+
+ @override
+ String repeatOnDaysSummary(String days) {
+ return 'on $days';
+ }
+
+ @override
+ String repeatOnMonthDaysSummary(String days) {
+ return 'on day $days';
+ }
+
+ @override
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
+
+ @override
+ String repeatInMonthsSummary(String months) {
+ return 'in $months';
+ }
+
+ @override
+ String repeatTimesSummary(int count) {
+ return '$count times';
+ }
+
+ @override
+ String repeatUntilSummary(String date) {
+ return 'until $date';
+ }
+
+ @override
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
+
@override
String get importance => 'Tähtsus';
@@ -912,6 +1211,26 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get createSubtask => 'Loo alamülesanne';
+ @override
+ String get subtasks => 'Alamülesanded';
+
+ @override
+ String get duplicateTask => 'Duplicate task';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => 'Hide subtasks';
+
+ @override
+ String get hideClosedSubtasks => 'Hide closed subtasks';
+
@override
String get moveToTop => 'Teisalda kõige üles';
@@ -923,7 +1242,7 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String deleteTaskConfirmation(String title) {
- return 'Kas kustutada „$title” Google Tasksist?';
+ return 'Kas kustutada „$title”?';
}
@override
@@ -1244,6 +1563,223 @@ class AppLocalizationsEt extends AppLocalizations {
@override
String get noLocationsFound => 'Asukohti ei leitud';
+ @override
+ String get requiredField => 'This field is required.';
+
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
+
+ @override
+ String get appleICloudProvider => 'Apple iCloud Calendar';
+
+ @override
+ String get nextcloudProvider => 'Nextcloud';
+
+ @override
+ String get appleICloudTasksProvider => 'Apple iCloud';
+
+ @override
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
+
+ @override
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
+
+ @override
+ String get addNextcloudAccount => 'Add Nextcloud account';
+
+ @override
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
+
+ @override
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
+
+ @override
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
+
+ @override
+ String get appleAccountEmail => 'Apple Account email';
+
+ @override
+ String get appleAppSpecificPassword => 'App-specific password';
+
+ @override
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
+
+ @override
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
+
+ @override
+ String get connectNextcloudTitle => 'Connect Nextcloud';
+
+ @override
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
+
+ @override
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
+
+ @override
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
+
+ @override
+ String get connectAccountAction => 'Connect';
+
+ @override
+ String get cancelAccountConnection => 'Cancel connection';
+
+ @override
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
+
+ @override
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
+
+ @override
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
+
+ @override
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
+
+ @override
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
+
+ @override
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
+
+ @override
+ String get collectionSettings => 'Calendars and task lists';
+
+ @override
+ String get calendarContent => 'Calendar events';
+
+ @override
+ String get taskContent => 'Tasks';
+
+ @override
+ String get readOnlySharedCollection => 'Read-only';
+
+ @override
+ String get pendingLocally => 'Pending locally';
+
+ @override
+ String get conflictBlocked => 'Blocked by conflict';
+
+ @override
+ String get authenticationBlocked => 'Blocked until reconnect';
+
+ @override
+ String get operationFailed => 'Operation failed';
+
+ @override
+ String get keepServerVersion => 'Keep server version';
+
+ @override
+ String get reapplyLocalChange => 'Review and reapply local change';
+
+ @override
+ String get duplicateLocalItem => 'Duplicate as new item';
+
+ @override
+ String get davConnectionState => 'Connection state';
+
+ @override
+ String get davConnected => 'Connected';
+
+ @override
+ String get davConnecting => 'Connecting…';
+
+ @override
+ String get davSignedOut => 'Signed out';
+
+ @override
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
+
+ @override
+ String get thisAndFutureUnavailable => 'This and future (not available)';
+
+ @override
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
+
+ @override
+ String get taskDueBeforeStart => 'Tähtaeg ei tohi olla enne algust.';
+
+ @override
+ String get taskStartDueTimeModeMismatch =>
+ 'Määra nii algus- kui ka tähtaja kellaaeg või tee ülesanne kogu päeva ülesandeks.';
+
@override
String deleteCalendarConfirmation(String title) {
return 'Kas kustutada „$title”?';
diff --git a/lib/l10n/generated/app_localizations_fa.dart b/lib/l10n/generated/app_localizations_fa.dart
index 447985d..48907da 100644
--- a/lib/l10n/generated/app_localizations_fa.dart
+++ b/lib/l10n/generated/app_localizations_fa.dart
@@ -17,7 +17,7 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get connectGoogleAccount =>
- 'حسابهای Google و Microsoft را متصل کنید تا تقویمها و کارها همگام شوند.';
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
@override
String get googlePermissionsConsentNotice =>
@@ -41,7 +41,7 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get onboardingAccountsStepDescription =>
- 'همهٔ حسابهای Google و Microsoft موردنظرتان را اضافه کنید. BusyMax تقویمها، رویدادها، فهرستهای کار و کارهای هر حساب را همگام میکند.';
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
@override
String get onboardingPreferencesStepTitle => 'انتخاب تنظیمات سیستم';
@@ -630,7 +630,7 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get removeAccountConfirmation =>
- 'با این کار، کارها، تقویمها، رویدادها، یادآورها و تغییرات آفلاین در انتظار از حافظهٔ نهان این دستگاه حذف میشوند. تغییرات همگامنشده از دست میروند. هیچ چیزی از Google یا Microsoft حذف نمیشود.';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess =>
@@ -651,7 +651,22 @@ class AppLocalizationsFa extends AppLocalizations {
'حساب از این دستگاه حذف شد، اما BusyMax نتوانست دسترسی Google را لغو کند. میتوانید آن را از حساب Google خود لغو کنید.';
@override
- String get newList => 'فهرست جدید';
+ String get newTaskList => 'فهرست کار جدید';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
@override
String get signInToViewTaskLists => 'برای دیدن فهرستهای کار وارد شوید.';
@@ -674,6 +689,17 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get deleteList => 'حذف فهرست';
+ @override
+ String get unshare => 'لغو اشتراکگذاری';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
@override
String get builtInMicrosoftList => 'داخلی';
@@ -686,6 +712,16 @@ class AppLocalizationsFa extends AppLocalizations {
return '«$title» از Google Tasks حذف شود؟';
}
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
@override
String get deleteEvent => 'حذف رویداد';
@@ -810,6 +846,74 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get doneStatus => 'انجامشده';
+ @override
+ String get taskStatus => 'Status';
+
+ @override
+ String get taskStatusNone => 'No status';
+
+ @override
+ String get taskStatusNeedsAction => 'نیاز به اقدام';
+
+ @override
+ String get taskStatusInProcess => 'در حال انجام';
+
+ @override
+ String get taskStatusCompleted => 'انجامشده';
+
+ @override
+ String get taskStatusCancelled => 'Cancelled';
+
+ @override
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
+
+ @override
+ String get completionDate => 'Completion date';
+
+ @override
+ String get priority => 'اولویت';
+
+ @override
+ String get priorityNone => 'No priority';
+
+ @override
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
+
+ @override
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
+
+ @override
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
+
+ @override
+ String get taskUrl => 'URL';
+
+ @override
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
+
+ @override
+ String get classification => 'Classification';
+
+ @override
+ String get classificationPublic => 'When shared, show the full task';
+
+ @override
+ String get classificationConfidential => 'When shared, show only busy';
+
+ @override
+ String get classificationPrivate => 'When shared, hide this task';
+
+ @override
+ String get pinTask => 'Pin task';
+
@override
String get notes => 'یادداشتها';
@@ -846,6 +950,84 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get addReminder => 'افزودن یادآور';
+ @override
+ String get reminders => 'Reminders';
+
+ @override
+ String get noReminders => 'بدون یادآوری';
+
+ @override
+ String get editReminder => 'Edit reminder';
+
+ @override
+ String get beforeTaskStarts => 'قبل از شروع کار';
+
+ @override
+ String get beforeTaskDue => 'قبل از موعد انجام کار';
+
+ @override
+ String get afterTaskStarts => 'After the task starts';
+
+ @override
+ String get afterTaskDue => 'After the task is due';
+
+ @override
+ String get relativeToTaskStart => 'Relative to the task start date';
+
+ @override
+ String get relativeToTaskDue => 'Relative to the task due date';
+
+ @override
+ String get reminderTimeOfDay => 'Time of day';
+
+ @override
+ String get absoluteReminder => 'At a date and time';
+
+ @override
+ String get reminderAmount => 'Amount';
+
+ @override
+ String get reminderUnit => 'Unit';
+
+ @override
+ String get reminderUnitSeconds => 'Seconds';
+
+ @override
+ String get reminderUnitMinutes => 'Minutes';
+
+ @override
+ String get reminderUnitHours => 'Hours';
+
+ @override
+ String get reminderUnitDays => 'Days';
+
+ @override
+ String get reminderUnitWeeks => 'Weeks';
+
+ @override
+ String get reminderAtTaskStart => 'At the task start';
+
+ @override
+ String get reminderAtTaskDue => 'At the task due time';
+
+ @override
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
+
+ @override
+ String get relatedRemindersTitle => 'Keep related reminders?';
+
+ @override
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
+
+ @override
+ String get discardRelatedReminders => 'حذف یادآوریها';
+
+ @override
+ String get keepRelatedReminders => 'نگهداشتن یادآوریها';
+
@override
String get addGuest => 'افزودن مهمان';
@@ -879,6 +1061,123 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get repeatYearly => 'سالانه';
+ @override
+ String get repeatEvery => 'تکرار هر';
+
+ @override
+ String get repeatOn => 'Repeat on';
+
+ @override
+ String get repeatEnd => 'پایان تکرار';
+
+ @override
+ String get repeatNever => 'Never';
+
+ @override
+ String get repeatUntil => 'On date';
+
+ @override
+ String get repeatAfter => 'After a number of occurrences';
+
+ @override
+ String get repeatCount => 'Occurrences';
+
+ @override
+ String get repeatDayOfMonth => 'Days of month';
+
+ @override
+ String get repeatMonths => 'Months';
+
+ @override
+ String get repeatOrdinal => 'Weekday position';
+
+ @override
+ String get repeatSpecificDays => 'Specific days';
+
+ @override
+ String get repeatFirst => 'First';
+
+ @override
+ String get repeatSecond => 'Second';
+
+ @override
+ String get repeatThird => 'Third';
+
+ @override
+ String get repeatFourth => 'Fourth';
+
+ @override
+ String get repeatFifth => 'Fifth';
+
+ @override
+ String get repeatSecondToLast => 'Second to last';
+
+ @override
+ String get repeatLast => 'Last';
+
+ @override
+ String get repeatAnyDay => 'Day';
+
+ @override
+ String get repeatWeekday => 'Weekday';
+
+ @override
+ String get repeatWeekendDay => 'Weekend day';
+
+ @override
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
+
+ @override
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
+
+ @override
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
+
+ @override
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
+
+ @override
+ String repeatOnDaysSummary(String days) {
+ return 'در $days';
+ }
+
+ @override
+ String repeatOnMonthDaysSummary(String days) {
+ return 'در روز $days';
+ }
+
+ @override
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
+
+ @override
+ String repeatInMonthsSummary(String months) {
+ return 'در $months';
+ }
+
+ @override
+ String repeatTimesSummary(int count) {
+ return '$count بار';
+ }
+
+ @override
+ String repeatUntilSummary(String date) {
+ return 'تا $date';
+ }
+
+ @override
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
+
@override
String get importance => 'اهمیت';
@@ -928,6 +1227,26 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get createSubtask => 'ایجاد زیرکار';
+ @override
+ String get subtasks => 'زیرکارها';
+
+ @override
+ String get duplicateTask => 'تکراریسازی وظیفه';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => 'مخفیسازی زیروظیفهها';
+
+ @override
+ String get hideClosedSubtasks => 'مخفیسازی زیروظیفههای بستهشده';
+
@override
String get moveToTop => 'انتقال به بالاترین جایگاه';
@@ -939,7 +1258,7 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String deleteTaskConfirmation(String title) {
- return '«$title» از Google Tasks حذف شود؟';
+ return '«$title» حذف شود؟';
}
@override
@@ -1279,6 +1598,223 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get noLocationsFound => 'مکانی پیدا نشد';
+ @override
+ String get requiredField => 'This field is required.';
+
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
+
+ @override
+ String get appleICloudProvider => 'Apple iCloud Calendar';
+
+ @override
+ String get nextcloudProvider => 'Nextcloud';
+
+ @override
+ String get appleICloudTasksProvider => 'Apple iCloud';
+
+ @override
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
+
+ @override
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
+
+ @override
+ String get addNextcloudAccount => 'Add Nextcloud account';
+
+ @override
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
+
+ @override
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
+
+ @override
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
+
+ @override
+ String get appleAccountEmail => 'Apple Account email';
+
+ @override
+ String get appleAppSpecificPassword => 'App-specific password';
+
+ @override
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
+
+ @override
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
+
+ @override
+ String get connectNextcloudTitle => 'Connect Nextcloud';
+
+ @override
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
+
+ @override
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
+
+ @override
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
+
+ @override
+ String get connectAccountAction => 'Connect';
+
+ @override
+ String get cancelAccountConnection => 'Cancel connection';
+
+ @override
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
+
+ @override
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
+
+ @override
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
+
+ @override
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
+
+ @override
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
+
+ @override
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
+
+ @override
+ String get collectionSettings => 'Calendars and task lists';
+
+ @override
+ String get calendarContent => 'Calendar events';
+
+ @override
+ String get taskContent => 'Tasks';
+
+ @override
+ String get readOnlySharedCollection => 'Read-only';
+
+ @override
+ String get pendingLocally => 'Pending locally';
+
+ @override
+ String get conflictBlocked => 'Blocked by conflict';
+
+ @override
+ String get authenticationBlocked => 'Blocked until reconnect';
+
+ @override
+ String get operationFailed => 'Operation failed';
+
+ @override
+ String get keepServerVersion => 'Keep server version';
+
+ @override
+ String get reapplyLocalChange => 'Review and reapply local change';
+
+ @override
+ String get duplicateLocalItem => 'Duplicate as new item';
+
+ @override
+ String get davConnectionState => 'Connection state';
+
+ @override
+ String get davConnected => 'Connected';
+
+ @override
+ String get davConnecting => 'Connecting…';
+
+ @override
+ String get davSignedOut => 'Signed out';
+
+ @override
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
+
+ @override
+ String get thisAndFutureUnavailable => 'This and future (not available)';
+
+ @override
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
+
+ @override
+ String get taskDueBeforeStart => 'زمان سررسید نباید پیش از زمان شروع باشد.';
+
+ @override
+ String get taskStartDueTimeModeMismatch =>
+ 'برای شروع و سررسید هر دو زمان تعیین کنید، یا کار را تمامروز کنید.';
+
@override
String deleteCalendarConfirmation(String title) {
return '«$title» حذف شود؟';
diff --git a/lib/l10n/generated/app_localizations_fi.dart b/lib/l10n/generated/app_localizations_fi.dart
index 5f5a3ee..10f91fb 100644
--- a/lib/l10n/generated/app_localizations_fi.dart
+++ b/lib/l10n/generated/app_localizations_fi.dart
@@ -17,7 +17,7 @@ class AppLocalizationsFi extends AppLocalizations {
@override
String get connectGoogleAccount =>
- 'Yhdistä Google- ja Microsoft-tilit kalenterien ja tehtävien synkronointia varten.';
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
@override
String get googlePermissionsConsentNotice =>
@@ -41,7 +41,7 @@ class AppLocalizationsFi extends AppLocalizations {
@override
String get onboardingAccountsStepDescription =>
- 'Lisää kaikki haluamasi Google- ja Microsoft-tilit. BusyMax synkronoi kunkin tilin kalenterit, tapahtumat, tehtäväluettelot ja tehtävät.';
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
@override
String get onboardingPreferencesStepTitle => 'Valitse järjestelmäasetukset';
@@ -614,7 +614,7 @@ class AppLocalizationsFi extends AppLocalizations {
@override
String get removeAccountConfirmation =>
- 'Tämä poistaa välimuistissa olevat tehtävät, kalenterit, tapahtumat, muistutukset ja odottavat offline-muutokset tältä laitteelta. Synkronoimattomat muutokset menetetään. Mitään ei poisteta Googlesta tai Microsoftista.';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess =>
@@ -636,7 +636,22 @@ class AppLocalizationsFi extends AppLocalizations {
'Tili poistettiin tältä laitteelta, mutta BusyMax ei voinut peruuttaa Google-käyttöoikeutta. Voit peruuttaa sen Google-tililtäsi.';
@override
- String get newList => 'Uusi luettelo';
+ String get newTaskList => 'Uusi tehtäväluettelo';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
@override
String get signInToViewTaskLists =>
@@ -661,6 +676,17 @@ class AppLocalizationsFi extends AppLocalizations {
@override
String get deleteList => 'Poista luettelo';
+ @override
+ String get unshare => 'Lopeta jakaminen';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
@override
String get builtInMicrosoftList => 'Sisäänrakennettu';
@@ -673,6 +699,16 @@ class AppLocalizationsFi extends AppLocalizations {
return 'Poistetaanko \"$title\" Google Tasksista?';
}
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
@override
String get deleteEvent => 'Poista tapahtuma';
@@ -797,6 +833,74 @@ class AppLocalizationsFi extends AppLocalizations {
@override
String get doneStatus => 'Valmis';
+ @override
+ String get taskStatus => 'Status';
+
+ @override
+ String get taskStatusNone => 'No status';
+
+ @override
+ String get taskStatusNeedsAction => 'Vaatii toimenpiteitä';
+
+ @override
+ String get taskStatusInProcess => 'Käsittelyssä';
+
+ @override
+ String get taskStatusCompleted => 'Valmiina';
+
+ @override
+ String get taskStatusCancelled => 'Cancelled';
+
+ @override
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
+
+ @override
+ String get completionDate => 'Completion date';
+
+ @override
+ String get priority => 'Tärkeys';
+
+ @override
+ String get priorityNone => 'No priority';
+
+ @override
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
+
+ @override
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
+
+ @override
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
+
+ @override
+ String get taskUrl => 'URL';
+
+ @override
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
+
+ @override
+ String get classification => 'Classification';
+
+ @override
+ String get classificationPublic => 'When shared, show the full task';
+
+ @override
+ String get classificationConfidential => 'When shared, show only busy';
+
+ @override
+ String get classificationPrivate => 'When shared, hide this task';
+
+ @override
+ String get pinTask => 'Pin task';
+
@override
String get notes => 'Muistiinpanot';
@@ -833,6 +937,84 @@ class AppLocalizationsFi extends AppLocalizations {
@override
String get addReminder => 'Lisää muistutus';
+ @override
+ String get reminders => 'Reminders';
+
+ @override
+ String get noReminders => 'No reminders';
+
+ @override
+ String get editReminder => 'Edit reminder';
+
+ @override
+ String get beforeTaskStarts => 'Before the task starts';
+
+ @override
+ String get beforeTaskDue => 'Before the task is due';
+
+ @override
+ String get afterTaskStarts => 'After the task starts';
+
+ @override
+ String get afterTaskDue => 'After the task is due';
+
+ @override
+ String get relativeToTaskStart => 'Relative to the task start date';
+
+ @override
+ String get relativeToTaskDue => 'Relative to the task due date';
+
+ @override
+ String get reminderTimeOfDay => 'Time of day';
+
+ @override
+ String get absoluteReminder => 'At a date and time';
+
+ @override
+ String get reminderAmount => 'Amount';
+
+ @override
+ String get reminderUnit => 'Unit';
+
+ @override
+ String get reminderUnitSeconds => 'Seconds';
+
+ @override
+ String get reminderUnitMinutes => 'Minutes';
+
+ @override
+ String get reminderUnitHours => 'Hours';
+
+ @override
+ String get reminderUnitDays => 'Days';
+
+ @override
+ String get reminderUnitWeeks => 'Weeks';
+
+ @override
+ String get reminderAtTaskStart => 'At the task start';
+
+ @override
+ String get reminderAtTaskDue => 'At the task due time';
+
+ @override
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
+
+ @override
+ String get relatedRemindersTitle => 'Keep related reminders?';
+
+ @override
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
+
+ @override
+ String get discardRelatedReminders => 'Discard reminders';
+
+ @override
+ String get keepRelatedReminders => 'Keep reminders';
+
@override
String get addGuest => 'Lisää vieras';
@@ -866,6 +1048,123 @@ class AppLocalizationsFi extends AppLocalizations {
@override
String get repeatYearly => 'Vuosittain';
+ @override
+ String get repeatEvery => 'Toista joka';
+
+ @override
+ String get repeatOn => 'Repeat on';
+
+ @override
+ String get repeatEnd => 'Lopeta toisto';
+
+ @override
+ String get repeatNever => 'Never';
+
+ @override
+ String get repeatUntil => 'On date';
+
+ @override
+ String get repeatAfter => 'After a number of occurrences';
+
+ @override
+ String get repeatCount => 'Occurrences';
+
+ @override
+ String get repeatDayOfMonth => 'Days of month';
+
+ @override
+ String get repeatMonths => 'Months';
+
+ @override
+ String get repeatOrdinal => 'Weekday position';
+
+ @override
+ String get repeatSpecificDays => 'Specific days';
+
+ @override
+ String get repeatFirst => 'First';
+
+ @override
+ String get repeatSecond => 'Second';
+
+ @override
+ String get repeatThird => 'Third';
+
+ @override
+ String get repeatFourth => 'Fourth';
+
+ @override
+ String get repeatFifth => 'Fifth';
+
+ @override
+ String get repeatSecondToLast => 'Second to last';
+
+ @override
+ String get repeatLast => 'Last';
+
+ @override
+ String get repeatAnyDay => 'Day';
+
+ @override
+ String get repeatWeekday => 'Weekday';
+
+ @override
+ String get repeatWeekendDay => 'Weekend day';
+
+ @override
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
+
+ @override
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
+
+ @override
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
+
+ @override
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
+
+ @override
+ String repeatOnDaysSummary(String days) {
+ return 'on $days';
+ }
+
+ @override
+ String repeatOnMonthDaysSummary(String days) {
+ return 'päivänä $days';
+ }
+
+ @override
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
+
+ @override
+ String repeatInMonthsSummary(String months) {
+ return 'in $months';
+ }
+
+ @override
+ String repeatTimesSummary(int count) {
+ return '$count kertaa';
+ }
+
+ @override
+ String repeatUntilSummary(String date) {
+ return '$date asti';
+ }
+
+ @override
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
+
@override
String get importance => 'Tärkeys';
@@ -915,6 +1214,26 @@ class AppLocalizationsFi extends AppLocalizations {
@override
String get createSubtask => 'Luo alitehtävä';
+ @override
+ String get subtasks => 'Alitehtävät';
+
+ @override
+ String get duplicateTask => 'Duplicate task';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => 'Piilota alitehtävät';
+
+ @override
+ String get hideClosedSubtasks => 'Piilota suljetut alitehtävät';
+
@override
String get moveToTop => 'Siirrä ylimmäksi';
@@ -926,7 +1245,7 @@ class AppLocalizationsFi extends AppLocalizations {
@override
String deleteTaskConfirmation(String title) {
- return 'Poistetaanko \"$title\" Google Tasksista?';
+ return 'Poistetaanko \"$title\"?';
}
@override
@@ -1249,6 +1568,223 @@ class AppLocalizationsFi extends AppLocalizations {
@override
String get noLocationsFound => 'Sijainteja ei löytynyt';
+ @override
+ String get requiredField => 'This field is required.';
+
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
+
+ @override
+ String get appleICloudProvider => 'Apple iCloud Calendar';
+
+ @override
+ String get nextcloudProvider => 'Nextcloud';
+
+ @override
+ String get appleICloudTasksProvider => 'Apple iCloud';
+
+ @override
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
+
+ @override
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
+
+ @override
+ String get addNextcloudAccount => 'Add Nextcloud account';
+
+ @override
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
+
+ @override
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
+
+ @override
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
+
+ @override
+ String get appleAccountEmail => 'Apple Account email';
+
+ @override
+ String get appleAppSpecificPassword => 'App-specific password';
+
+ @override
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
+
+ @override
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
+
+ @override
+ String get connectNextcloudTitle => 'Connect Nextcloud';
+
+ @override
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
+
+ @override
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
+
+ @override
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
+
+ @override
+ String get connectAccountAction => 'Connect';
+
+ @override
+ String get cancelAccountConnection => 'Cancel connection';
+
+ @override
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
+
+ @override
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
+
+ @override
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
+
+ @override
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
+
+ @override
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
+
+ @override
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
+
+ @override
+ String get collectionSettings => 'Calendars and task lists';
+
+ @override
+ String get calendarContent => 'Calendar events';
+
+ @override
+ String get taskContent => 'Tasks';
+
+ @override
+ String get readOnlySharedCollection => 'Read-only';
+
+ @override
+ String get pendingLocally => 'Pending locally';
+
+ @override
+ String get conflictBlocked => 'Blocked by conflict';
+
+ @override
+ String get authenticationBlocked => 'Blocked until reconnect';
+
+ @override
+ String get operationFailed => 'Operation failed';
+
+ @override
+ String get keepServerVersion => 'Keep server version';
+
+ @override
+ String get reapplyLocalChange => 'Review and reapply local change';
+
+ @override
+ String get duplicateLocalItem => 'Duplicate as new item';
+
+ @override
+ String get davConnectionState => 'Connection state';
+
+ @override
+ String get davConnected => 'Connected';
+
+ @override
+ String get davConnecting => 'Connecting…';
+
+ @override
+ String get davSignedOut => 'Signed out';
+
+ @override
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
+
+ @override
+ String get thisAndFutureUnavailable => 'This and future (not available)';
+
+ @override
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
+
+ @override
+ String get taskDueBeforeStart => 'Määräaika ei saa olla ennen alkamisaikaa.';
+
+ @override
+ String get taskStartDueTimeModeMismatch =>
+ 'Aseta kellonaika sekä alkamiselle että määräajalle tai tee tehtävästä koko päivän tehtävä.';
+
@override
String deleteCalendarConfirmation(String title) {
return 'Poistetaanko \"$title\"?';
diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart
index 17b96a0..cf80e22 100644
--- a/lib/l10n/generated/app_localizations_fr.dart
+++ b/lib/l10n/generated/app_localizations_fr.dart
@@ -17,7 +17,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get connectGoogleAccount =>
- 'Connectez des comptes Google et Microsoft pour synchroniser calendriers et tâches.';
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
@override
String get googlePermissionsConsentNotice =>
@@ -41,7 +41,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get onboardingAccountsStepDescription =>
- 'Ajoutez tous les comptes Google et Microsoft que vous voulez utiliser. BusyMax synchronise les calendriers, événements, listes de tâches et tâches de chaque compte.';
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
@override
String get onboardingPreferencesStepTitle => 'Choisir les paramètres système';
@@ -613,7 +613,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get removeAccountConfirmation =>
- 'Cette action supprime de cet appareil les tâches, calendriers, événements, rappels et modifications hors ligne en attente mis en cache. Les modifications non synchronisées seront perdues. Aucune donnée ne sera supprimée de Google ou Microsoft.';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess =>
@@ -635,7 +635,22 @@ class AppLocalizationsFr extends AppLocalizations {
'Le compte a été supprimé de cet appareil, mais BusyMax n’a pas pu révoquer son accès à votre compte Google. Vous pouvez révoquer cet accès depuis votre compte Google.';
@override
- String get newList => 'Nouvelle liste';
+ String get newTaskList => 'Nouvelle liste de tâches';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
@override
String get signInToViewTaskLists =>
@@ -659,6 +674,17 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get deleteList => 'Supprimer la liste';
+ @override
+ String get unshare => 'Ne plus partager';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
@override
String get builtInMicrosoftList => 'Intégrée';
@@ -671,6 +697,16 @@ class AppLocalizationsFr extends AppLocalizations {
return 'Supprimer « $title » de Google Tasks ?';
}
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
@override
String get deleteEvent => 'Supprimer l’événement';
@@ -795,6 +831,74 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get doneStatus => 'Terminée';
+ @override
+ String get taskStatus => 'Status';
+
+ @override
+ String get taskStatusNone => 'No status';
+
+ @override
+ String get taskStatusNeedsAction => 'Nécessite une action';
+
+ @override
+ String get taskStatusInProcess => 'En cours';
+
+ @override
+ String get taskStatusCompleted => 'Terminé';
+
+ @override
+ String get taskStatusCancelled => 'Cancelled';
+
+ @override
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
+
+ @override
+ String get completionDate => 'Completion date';
+
+ @override
+ String get priority => 'Priorité';
+
+ @override
+ String get priorityNone => 'No priority';
+
+ @override
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
+
+ @override
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
+
+ @override
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
+
+ @override
+ String get taskUrl => 'URL';
+
+ @override
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
+
+ @override
+ String get classification => 'Classification';
+
+ @override
+ String get classificationPublic => 'When shared, show the full task';
+
+ @override
+ String get classificationConfidential => 'When shared, show only busy';
+
+ @override
+ String get classificationPrivate => 'When shared, hide this task';
+
+ @override
+ String get pinTask => 'Pin task';
+
@override
String get notes => 'Notes';
@@ -831,6 +935,84 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get addReminder => 'Ajouter un rappel';
+ @override
+ String get reminders => 'Reminders';
+
+ @override
+ String get noReminders => 'Aucun rappel';
+
+ @override
+ String get editReminder => 'Edit reminder';
+
+ @override
+ String get beforeTaskStarts => 'Avant le début de la tâche';
+
+ @override
+ String get beforeTaskDue => 'Avant l\'échéance de la tâche';
+
+ @override
+ String get afterTaskStarts => 'After the task starts';
+
+ @override
+ String get afterTaskDue => 'After the task is due';
+
+ @override
+ String get relativeToTaskStart => 'Relative to the task start date';
+
+ @override
+ String get relativeToTaskDue => 'Relative to the task due date';
+
+ @override
+ String get reminderTimeOfDay => 'Time of day';
+
+ @override
+ String get absoluteReminder => 'At a date and time';
+
+ @override
+ String get reminderAmount => 'Amount';
+
+ @override
+ String get reminderUnit => 'Unit';
+
+ @override
+ String get reminderUnitSeconds => 'Seconds';
+
+ @override
+ String get reminderUnitMinutes => 'Minutes';
+
+ @override
+ String get reminderUnitHours => 'Hours';
+
+ @override
+ String get reminderUnitDays => 'Days';
+
+ @override
+ String get reminderUnitWeeks => 'Weeks';
+
+ @override
+ String get reminderAtTaskStart => 'At the task start';
+
+ @override
+ String get reminderAtTaskDue => 'At the task due time';
+
+ @override
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
+
+ @override
+ String get relatedRemindersTitle => 'Keep related reminders?';
+
+ @override
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
+
+ @override
+ String get discardRelatedReminders => 'Discard reminders';
+
+ @override
+ String get keepRelatedReminders => 'Keep reminders';
+
@override
String get addGuest => 'Ajouter un invité';
@@ -864,6 +1046,123 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeatYearly => 'Annuel';
+ @override
+ String get repeatEvery => 'Répéter chaque';
+
+ @override
+ String get repeatOn => 'Repeat on';
+
+ @override
+ String get repeatEnd => 'Arrêter la répétition';
+
+ @override
+ String get repeatNever => 'Never';
+
+ @override
+ String get repeatUntil => 'On date';
+
+ @override
+ String get repeatAfter => 'After a number of occurrences';
+
+ @override
+ String get repeatCount => 'Occurrences';
+
+ @override
+ String get repeatDayOfMonth => 'Days of month';
+
+ @override
+ String get repeatMonths => 'Months';
+
+ @override
+ String get repeatOrdinal => 'Weekday position';
+
+ @override
+ String get repeatSpecificDays => 'Specific days';
+
+ @override
+ String get repeatFirst => 'First';
+
+ @override
+ String get repeatSecond => 'Second';
+
+ @override
+ String get repeatThird => 'Third';
+
+ @override
+ String get repeatFourth => 'Fourth';
+
+ @override
+ String get repeatFifth => 'Fifth';
+
+ @override
+ String get repeatSecondToLast => 'Second to last';
+
+ @override
+ String get repeatLast => 'Last';
+
+ @override
+ String get repeatAnyDay => 'Day';
+
+ @override
+ String get repeatWeekday => 'Weekday';
+
+ @override
+ String get repeatWeekendDay => 'Weekend day';
+
+ @override
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
+
+ @override
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
+
+ @override
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
+
+ @override
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
+
+ @override
+ String repeatOnDaysSummary(String days) {
+ return 'on $days';
+ }
+
+ @override
+ String repeatOnMonthDaysSummary(String days) {
+ return 'le $days';
+ }
+
+ @override
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
+
+ @override
+ String repeatInMonthsSummary(String months) {
+ return 'in $months';
+ }
+
+ @override
+ String repeatTimesSummary(int count) {
+ return '$count fois';
+ }
+
+ @override
+ String repeatUntilSummary(String date) {
+ return 'jusqu\'au $date';
+ }
+
+ @override
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
+
@override
String get importance => 'Importance';
@@ -913,6 +1212,26 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get createSubtask => 'Créer une sous-tâche';
+ @override
+ String get subtasks => 'Sous-tâches';
+
+ @override
+ String get duplicateTask => 'Duplicate task';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => 'Masquer les sous-tâches';
+
+ @override
+ String get hideClosedSubtasks => 'Masquer les sous-tâches fermées';
+
@override
String get moveToTop => 'Déplacer tout en haut';
@@ -924,7 +1243,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String deleteTaskConfirmation(String title) {
- return 'Supprimer « $title » de Google Tasks ?';
+ return 'Supprimer « $title » ?';
}
@override
@@ -1252,6 +1571,223 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get noLocationsFound => 'Aucun lieu trouvé';
+ @override
+ String get requiredField => 'This field is required.';
+
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
+
+ @override
+ String get appleICloudProvider => 'Apple iCloud Calendar';
+
+ @override
+ String get nextcloudProvider => 'Nextcloud';
+
+ @override
+ String get appleICloudTasksProvider => 'Apple iCloud';
+
+ @override
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
+
+ @override
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
+
+ @override
+ String get addNextcloudAccount => 'Add Nextcloud account';
+
+ @override
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
+
+ @override
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
+
+ @override
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
+
+ @override
+ String get appleAccountEmail => 'Apple Account email';
+
+ @override
+ String get appleAppSpecificPassword => 'App-specific password';
+
+ @override
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
+
+ @override
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
+
+ @override
+ String get connectNextcloudTitle => 'Connect Nextcloud';
+
+ @override
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
+
+ @override
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
+
+ @override
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
+
+ @override
+ String get connectAccountAction => 'Connect';
+
+ @override
+ String get cancelAccountConnection => 'Cancel connection';
+
+ @override
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
+
+ @override
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
+
+ @override
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
+
+ @override
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
+
+ @override
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
+
+ @override
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
+
+ @override
+ String get collectionSettings => 'Calendars and task lists';
+
+ @override
+ String get calendarContent => 'Calendar events';
+
+ @override
+ String get taskContent => 'Tasks';
+
+ @override
+ String get readOnlySharedCollection => 'Read-only';
+
+ @override
+ String get pendingLocally => 'Pending locally';
+
+ @override
+ String get conflictBlocked => 'Blocked by conflict';
+
+ @override
+ String get authenticationBlocked => 'Blocked until reconnect';
+
+ @override
+ String get operationFailed => 'Operation failed';
+
+ @override
+ String get keepServerVersion => 'Keep server version';
+
+ @override
+ String get reapplyLocalChange => 'Review and reapply local change';
+
+ @override
+ String get duplicateLocalItem => 'Duplicate as new item';
+
+ @override
+ String get davConnectionState => 'Connection state';
+
+ @override
+ String get davConnected => 'Connected';
+
+ @override
+ String get davConnecting => 'Connecting…';
+
+ @override
+ String get davSignedOut => 'Signed out';
+
+ @override
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
+
+ @override
+ String get thisAndFutureUnavailable => 'This and future (not available)';
+
+ @override
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
+
+ @override
+ String get taskDueBeforeStart => 'L’échéance ne peut pas précéder le début.';
+
+ @override
+ String get taskStartDueTimeModeMismatch =>
+ 'Définissez une heure pour le début et l’échéance, ou passez la tâche en journée entière.';
+
@override
String deleteCalendarConfirmation(String title) {
return 'Supprimer « $title » ?';
diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart
index 71cb781..2a4c6f7 100644
--- a/lib/l10n/generated/app_localizations_hi.dart
+++ b/lib/l10n/generated/app_localizations_hi.dart
@@ -17,7 +17,7 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get connectGoogleAccount =>
- 'कैलेंडर और कार्य सिंक करने के लिए Google और Microsoft खाते कनेक्ट करें।';
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
@override
String get googlePermissionsConsentNotice =>
@@ -41,7 +41,7 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get onboardingAccountsStepDescription =>
- 'वे सभी Google और Microsoft खाते जोड़ें जिन्हें आप उपयोग करना चाहते हैं। BusyMax प्रत्येक खाते के कैलेंडर, ईवेंट, कार्य सूचियाँ और कार्य सिंक करता है।';
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
@override
String get onboardingPreferencesStepTitle => 'सिस्टम सेटिंग्स चुनें';
@@ -613,7 +613,7 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get removeAccountConfirmation =>
- 'इससे कैश किए गए कार्य, कैलेंडर, ईवेंट, रिमाइंडर और लंबित ऑफ़लाइन बदलाव इस डिवाइस से मिट जाएँगे। सिंक न किए गए बदलाव खो जाएँगे। Google या Microsoft से कुछ भी नहीं मिटेगा।';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess =>
@@ -635,7 +635,22 @@ class AppLocalizationsHi extends AppLocalizations {
'खाता इस डिवाइस से हटा दिया गया, लेकिन BusyMax की Google खाते तक पहुँच रद्द नहीं की जा सकी। आप यह पहुँच अपने Google खाते से रद्द कर सकते हैं।';
@override
- String get newList => 'नई सूची';
+ String get newTaskList => 'नई कार्य सूची';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
@override
String get signInToViewTaskLists =>
@@ -659,6 +674,17 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get deleteList => 'सूची मिटाएँ';
+ @override
+ String get unshare => 'Unshare';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
@override
String get builtInMicrosoftList => 'अंतर्निहित';
@@ -671,6 +697,16 @@ class AppLocalizationsHi extends AppLocalizations {
return 'Google Tasks से “$title” मिटाएँ?';
}
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
@override
String get deleteEvent => 'ईवेंट मिटाएँ';
@@ -795,6 +831,74 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get doneStatus => 'पूर्ण';
+ @override
+ String get taskStatus => 'Status';
+
+ @override
+ String get taskStatusNone => 'No status';
+
+ @override
+ String get taskStatusNeedsAction => 'Needs action';
+
+ @override
+ String get taskStatusInProcess => 'In process';
+
+ @override
+ String get taskStatusCompleted => 'Completed';
+
+ @override
+ String get taskStatusCancelled => 'Cancelled';
+
+ @override
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
+
+ @override
+ String get completionDate => 'Completion date';
+
+ @override
+ String get priority => 'Priority';
+
+ @override
+ String get priorityNone => 'No priority';
+
+ @override
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
+
+ @override
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
+
+ @override
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
+
+ @override
+ String get taskUrl => 'URL';
+
+ @override
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
+
+ @override
+ String get classification => 'Classification';
+
+ @override
+ String get classificationPublic => 'When shared, show the full task';
+
+ @override
+ String get classificationConfidential => 'When shared, show only busy';
+
+ @override
+ String get classificationPrivate => 'When shared, hide this task';
+
+ @override
+ String get pinTask => 'Pin task';
+
@override
String get notes => 'नोट्स';
@@ -831,6 +935,84 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get addReminder => 'रिमाइंडर जोड़ें';
+ @override
+ String get reminders => 'Reminders';
+
+ @override
+ String get noReminders => 'No reminders';
+
+ @override
+ String get editReminder => 'Edit reminder';
+
+ @override
+ String get beforeTaskStarts => 'Before the task starts';
+
+ @override
+ String get beforeTaskDue => 'Before the task is due';
+
+ @override
+ String get afterTaskStarts => 'After the task starts';
+
+ @override
+ String get afterTaskDue => 'After the task is due';
+
+ @override
+ String get relativeToTaskStart => 'Relative to the task start date';
+
+ @override
+ String get relativeToTaskDue => 'Relative to the task due date';
+
+ @override
+ String get reminderTimeOfDay => 'Time of day';
+
+ @override
+ String get absoluteReminder => 'At a date and time';
+
+ @override
+ String get reminderAmount => 'Amount';
+
+ @override
+ String get reminderUnit => 'Unit';
+
+ @override
+ String get reminderUnitSeconds => 'Seconds';
+
+ @override
+ String get reminderUnitMinutes => 'Minutes';
+
+ @override
+ String get reminderUnitHours => 'Hours';
+
+ @override
+ String get reminderUnitDays => 'Days';
+
+ @override
+ String get reminderUnitWeeks => 'Weeks';
+
+ @override
+ String get reminderAtTaskStart => 'At the task start';
+
+ @override
+ String get reminderAtTaskDue => 'At the task due time';
+
+ @override
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
+
+ @override
+ String get relatedRemindersTitle => 'Keep related reminders?';
+
+ @override
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
+
+ @override
+ String get discardRelatedReminders => 'Discard reminders';
+
+ @override
+ String get keepRelatedReminders => 'Keep reminders';
+
@override
String get addGuest => 'अतिथि जोड़ें';
@@ -864,6 +1046,123 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get repeatYearly => 'हर वर्ष';
+ @override
+ String get repeatEvery => 'Repeat every';
+
+ @override
+ String get repeatOn => 'Repeat on';
+
+ @override
+ String get repeatEnd => 'End repeat';
+
+ @override
+ String get repeatNever => 'Never';
+
+ @override
+ String get repeatUntil => 'On date';
+
+ @override
+ String get repeatAfter => 'After a number of occurrences';
+
+ @override
+ String get repeatCount => 'Occurrences';
+
+ @override
+ String get repeatDayOfMonth => 'Days of month';
+
+ @override
+ String get repeatMonths => 'Months';
+
+ @override
+ String get repeatOrdinal => 'Weekday position';
+
+ @override
+ String get repeatSpecificDays => 'Specific days';
+
+ @override
+ String get repeatFirst => 'First';
+
+ @override
+ String get repeatSecond => 'Second';
+
+ @override
+ String get repeatThird => 'Third';
+
+ @override
+ String get repeatFourth => 'Fourth';
+
+ @override
+ String get repeatFifth => 'Fifth';
+
+ @override
+ String get repeatSecondToLast => 'Second to last';
+
+ @override
+ String get repeatLast => 'Last';
+
+ @override
+ String get repeatAnyDay => 'Day';
+
+ @override
+ String get repeatWeekday => 'Weekday';
+
+ @override
+ String get repeatWeekendDay => 'Weekend day';
+
+ @override
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
+
+ @override
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
+
+ @override
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
+
+ @override
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
+
+ @override
+ String repeatOnDaysSummary(String days) {
+ return 'on $days';
+ }
+
+ @override
+ String repeatOnMonthDaysSummary(String days) {
+ return 'on day $days';
+ }
+
+ @override
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
+
+ @override
+ String repeatInMonthsSummary(String months) {
+ return 'in $months';
+ }
+
+ @override
+ String repeatTimesSummary(int count) {
+ return '$count times';
+ }
+
+ @override
+ String repeatUntilSummary(String date) {
+ return 'until $date';
+ }
+
+ @override
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
+
@override
String get importance => 'महत्त्व';
@@ -913,6 +1212,26 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get createSubtask => 'उपकार्य बनाएँ';
+ @override
+ String get subtasks => 'उपकार्य';
+
+ @override
+ String get duplicateTask => 'Duplicate task';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => 'Hide subtasks';
+
+ @override
+ String get hideClosedSubtasks => 'Hide closed subtasks';
+
@override
String get moveToTop => 'सबसे ऊपर ले जाएँ';
@@ -924,7 +1243,7 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String deleteTaskConfirmation(String title) {
- return 'Google Tasks से “$title” मिटाएँ?';
+ return '“$title” मिटाएँ?';
}
@override
@@ -1243,6 +1562,223 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get noLocationsFound => 'कोई स्थान नहीं मिला';
+ @override
+ String get requiredField => 'This field is required.';
+
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
+
+ @override
+ String get appleICloudProvider => 'Apple iCloud Calendar';
+
+ @override
+ String get nextcloudProvider => 'Nextcloud';
+
+ @override
+ String get appleICloudTasksProvider => 'Apple iCloud';
+
+ @override
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
+
+ @override
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
+
+ @override
+ String get addNextcloudAccount => 'Add Nextcloud account';
+
+ @override
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
+
+ @override
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
+
+ @override
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
+
+ @override
+ String get appleAccountEmail => 'Apple Account email';
+
+ @override
+ String get appleAppSpecificPassword => 'App-specific password';
+
+ @override
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
+
+ @override
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
+
+ @override
+ String get connectNextcloudTitle => 'Connect Nextcloud';
+
+ @override
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
+
+ @override
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
+
+ @override
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
+
+ @override
+ String get connectAccountAction => 'Connect';
+
+ @override
+ String get cancelAccountConnection => 'Cancel connection';
+
+ @override
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
+
+ @override
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
+
+ @override
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
+
+ @override
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
+
+ @override
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
+
+ @override
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
+
+ @override
+ String get collectionSettings => 'Calendars and task lists';
+
+ @override
+ String get calendarContent => 'Calendar events';
+
+ @override
+ String get taskContent => 'Tasks';
+
+ @override
+ String get readOnlySharedCollection => 'Read-only';
+
+ @override
+ String get pendingLocally => 'Pending locally';
+
+ @override
+ String get conflictBlocked => 'Blocked by conflict';
+
+ @override
+ String get authenticationBlocked => 'Blocked until reconnect';
+
+ @override
+ String get operationFailed => 'Operation failed';
+
+ @override
+ String get keepServerVersion => 'Keep server version';
+
+ @override
+ String get reapplyLocalChange => 'Review and reapply local change';
+
+ @override
+ String get duplicateLocalItem => 'Duplicate as new item';
+
+ @override
+ String get davConnectionState => 'Connection state';
+
+ @override
+ String get davConnected => 'Connected';
+
+ @override
+ String get davConnecting => 'Connecting…';
+
+ @override
+ String get davSignedOut => 'Signed out';
+
+ @override
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
+
+ @override
+ String get thisAndFutureUnavailable => 'This and future (not available)';
+
+ @override
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
+
+ @override
+ String get taskDueBeforeStart => 'नियत समय प्रारंभ समय से पहले नहीं हो सकता।';
+
+ @override
+ String get taskStartDueTimeModeMismatch =>
+ 'प्रारंभ और नियत समय दोनों सेट करें, या कार्य को पूरे दिन का बनाएँ।';
+
@override
String deleteCalendarConfirmation(String title) {
return '“$title” मिटाएँ?';
diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart
index 426438b..4ab26e1 100644
--- a/lib/l10n/generated/app_localizations_it.dart
+++ b/lib/l10n/generated/app_localizations_it.dart
@@ -17,7 +17,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get connectGoogleAccount =>
- 'Collega gli account Google e Microsoft per sincronizzare calendari e attività.';
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
@override
String get googlePermissionsConsentNotice =>
@@ -41,7 +41,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get onboardingAccountsStepDescription =>
- 'Aggiungi tutti gli account Google e Microsoft che vuoi utilizzare. BusyMax sincronizza calendari, eventi, elenchi di attività e attività di ogni account.';
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
@override
String get onboardingPreferencesStepTitle =>
@@ -615,7 +615,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get removeAccountConfirmation =>
- 'Questa azione elimina dal dispositivo attività, calendari, eventi e promemoria memorizzati nella cache, oltre alle modifiche offline in sospeso. Le modifiche non sincronizzate andranno perse. Non verrà eliminato nulla da Google o Microsoft.';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess =>
@@ -637,7 +637,22 @@ class AppLocalizationsIt extends AppLocalizations {
'L’account è stato rimosso da questo dispositivo, ma BusyMax non è riuscito a revocare il proprio accesso a Google. Puoi revocarlo dal tuo account Google.';
@override
- String get newList => 'Nuovo elenco';
+ String get newTaskList => 'Nuovo elenco di attività';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
@override
String get signInToViewTaskLists =>
@@ -662,6 +677,17 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get deleteList => 'Elimina elenco';
+ @override
+ String get unshare => 'Rimuovi condivisione';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
@override
String get builtInMicrosoftList => 'Integrato';
@@ -674,6 +700,16 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Eliminare «$title» da Google Tasks?';
}
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
@override
String get deleteEvent => 'Elimina evento';
@@ -798,6 +834,74 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get doneStatus => 'Completata';
+ @override
+ String get taskStatus => 'Status';
+
+ @override
+ String get taskStatusNone => 'No status';
+
+ @override
+ String get taskStatusNeedsAction => 'Richiede azione';
+
+ @override
+ String get taskStatusInProcess => 'In corso';
+
+ @override
+ String get taskStatusCompleted => 'Completato';
+
+ @override
+ String get taskStatusCancelled => 'Cancelled';
+
+ @override
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
+
+ @override
+ String get completionDate => 'Completion date';
+
+ @override
+ String get priority => 'Priorità';
+
+ @override
+ String get priorityNone => 'No priority';
+
+ @override
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
+
+ @override
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
+
+ @override
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
+
+ @override
+ String get taskUrl => 'URL';
+
+ @override
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
+
+ @override
+ String get classification => 'Classification';
+
+ @override
+ String get classificationPublic => 'When shared, show the full task';
+
+ @override
+ String get classificationConfidential => 'When shared, show only busy';
+
+ @override
+ String get classificationPrivate => 'When shared, hide this task';
+
+ @override
+ String get pinTask => 'Pin task';
+
@override
String get notes => 'Note';
@@ -834,6 +938,84 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get addReminder => 'Aggiungi promemoria';
+ @override
+ String get reminders => 'Reminders';
+
+ @override
+ String get noReminders => 'Nessun promemoria';
+
+ @override
+ String get editReminder => 'Edit reminder';
+
+ @override
+ String get beforeTaskStarts => 'Prima dell\'inizio dell\'attività';
+
+ @override
+ String get beforeTaskDue => 'Prima della scadenza del compito';
+
+ @override
+ String get afterTaskStarts => 'After the task starts';
+
+ @override
+ String get afterTaskDue => 'After the task is due';
+
+ @override
+ String get relativeToTaskStart => 'Relative to the task start date';
+
+ @override
+ String get relativeToTaskDue => 'Relative to the task due date';
+
+ @override
+ String get reminderTimeOfDay => 'Time of day';
+
+ @override
+ String get absoluteReminder => 'At a date and time';
+
+ @override
+ String get reminderAmount => 'Amount';
+
+ @override
+ String get reminderUnit => 'Unit';
+
+ @override
+ String get reminderUnitSeconds => 'Seconds';
+
+ @override
+ String get reminderUnitMinutes => 'Minutes';
+
+ @override
+ String get reminderUnitHours => 'Hours';
+
+ @override
+ String get reminderUnitDays => 'Days';
+
+ @override
+ String get reminderUnitWeeks => 'Weeks';
+
+ @override
+ String get reminderAtTaskStart => 'At the task start';
+
+ @override
+ String get reminderAtTaskDue => 'At the task due time';
+
+ @override
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
+
+ @override
+ String get relatedRemindersTitle => 'Keep related reminders?';
+
+ @override
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
+
+ @override
+ String get discardRelatedReminders => 'Scarta i promemoria';
+
+ @override
+ String get keepRelatedReminders => 'Mantieni i promemoria';
+
@override
String get addGuest => 'Aggiungi invitato';
@@ -867,6 +1049,123 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get repeatYearly => 'Ogni anno';
+ @override
+ String get repeatEvery => 'Ripeti ogni';
+
+ @override
+ String get repeatOn => 'Repeat on';
+
+ @override
+ String get repeatEnd => 'Termina ripetizione';
+
+ @override
+ String get repeatNever => 'Never';
+
+ @override
+ String get repeatUntil => 'On date';
+
+ @override
+ String get repeatAfter => 'After a number of occurrences';
+
+ @override
+ String get repeatCount => 'Occurrences';
+
+ @override
+ String get repeatDayOfMonth => 'Days of month';
+
+ @override
+ String get repeatMonths => 'Months';
+
+ @override
+ String get repeatOrdinal => 'Weekday position';
+
+ @override
+ String get repeatSpecificDays => 'Specific days';
+
+ @override
+ String get repeatFirst => 'First';
+
+ @override
+ String get repeatSecond => 'Second';
+
+ @override
+ String get repeatThird => 'Third';
+
+ @override
+ String get repeatFourth => 'Fourth';
+
+ @override
+ String get repeatFifth => 'Fifth';
+
+ @override
+ String get repeatSecondToLast => 'Second to last';
+
+ @override
+ String get repeatLast => 'Last';
+
+ @override
+ String get repeatAnyDay => 'Day';
+
+ @override
+ String get repeatWeekday => 'Weekday';
+
+ @override
+ String get repeatWeekendDay => 'Weekend day';
+
+ @override
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
+
+ @override
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
+
+ @override
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
+
+ @override
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
+
+ @override
+ String repeatOnDaysSummary(String days) {
+ return 'on $days';
+ }
+
+ @override
+ String repeatOnMonthDaysSummary(String days) {
+ return 'il giorno $days';
+ }
+
+ @override
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
+
+ @override
+ String repeatInMonthsSummary(String months) {
+ return 'in $months';
+ }
+
+ @override
+ String repeatTimesSummary(int count) {
+ return '$count volte';
+ }
+
+ @override
+ String repeatUntilSummary(String date) {
+ return 'fino al $date';
+ }
+
+ @override
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
+
@override
String get importance => 'Importanza';
@@ -916,6 +1215,26 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get createSubtask => 'Crea sottoattività';
+ @override
+ String get subtasks => 'Sottoattività';
+
+ @override
+ String get duplicateTask => 'Duplicate task';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => 'Nascondi sottoattività';
+
+ @override
+ String get hideClosedSubtasks => 'Nascondi sotto-attività chiuse';
+
@override
String get moveToTop => 'Sposta in cima';
@@ -927,7 +1246,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String deleteTaskConfirmation(String title) {
- return 'Eliminare «$title» da Google Tasks?';
+ return 'Eliminare «$title»?';
}
@override
@@ -1255,6 +1574,223 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get noLocationsFound => 'Nessun luogo trovato';
+ @override
+ String get requiredField => 'This field is required.';
+
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
+
+ @override
+ String get appleICloudProvider => 'Apple iCloud Calendar';
+
+ @override
+ String get nextcloudProvider => 'Nextcloud';
+
+ @override
+ String get appleICloudTasksProvider => 'Apple iCloud';
+
+ @override
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
+
+ @override
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
+
+ @override
+ String get addNextcloudAccount => 'Add Nextcloud account';
+
+ @override
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
+
+ @override
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
+
+ @override
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
+
+ @override
+ String get appleAccountEmail => 'Apple Account email';
+
+ @override
+ String get appleAppSpecificPassword => 'App-specific password';
+
+ @override
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
+
+ @override
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
+
+ @override
+ String get connectNextcloudTitle => 'Connect Nextcloud';
+
+ @override
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
+
+ @override
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
+
+ @override
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
+
+ @override
+ String get connectAccountAction => 'Connect';
+
+ @override
+ String get cancelAccountConnection => 'Cancel connection';
+
+ @override
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
+
+ @override
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
+
+ @override
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
+
+ @override
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
+
+ @override
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
+
+ @override
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
+
+ @override
+ String get collectionSettings => 'Calendars and task lists';
+
+ @override
+ String get calendarContent => 'Calendar events';
+
+ @override
+ String get taskContent => 'Tasks';
+
+ @override
+ String get readOnlySharedCollection => 'Read-only';
+
+ @override
+ String get pendingLocally => 'Pending locally';
+
+ @override
+ String get conflictBlocked => 'Blocked by conflict';
+
+ @override
+ String get authenticationBlocked => 'Blocked until reconnect';
+
+ @override
+ String get operationFailed => 'Operation failed';
+
+ @override
+ String get keepServerVersion => 'Keep server version';
+
+ @override
+ String get reapplyLocalChange => 'Review and reapply local change';
+
+ @override
+ String get duplicateLocalItem => 'Duplicate as new item';
+
+ @override
+ String get davConnectionState => 'Connection state';
+
+ @override
+ String get davConnected => 'Connected';
+
+ @override
+ String get davConnecting => 'Connecting…';
+
+ @override
+ String get davSignedOut => 'Signed out';
+
+ @override
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
+
+ @override
+ String get thisAndFutureUnavailable => 'This and future (not available)';
+
+ @override
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
+
+ @override
+ String get taskDueBeforeStart => 'La scadenza non può precedere l\'inizio.';
+
+ @override
+ String get taskStartDueTimeModeMismatch =>
+ 'Imposta un orario sia per l\'inizio sia per la scadenza, oppure rendi l\'attività valida per l\'intera giornata.';
+
@override
String deleteCalendarConfirmation(String title) {
return 'Eliminare «$title»?';
diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart
index 1da583e..7dafd84 100644
--- a/lib/l10n/generated/app_localizations_ja.dart
+++ b/lib/l10n/generated/app_localizations_ja.dart
@@ -17,7 +17,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get connectGoogleAccount =>
- 'Google と Microsoft のアカウントを接続して、カレンダーとタスクを同期します。';
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
@override
String get googlePermissionsConsentNotice =>
@@ -41,7 +41,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get onboardingAccountsStepDescription =>
- '使用するすべての Google アカウントと Microsoft アカウントを追加してください。BusyMax は各アカウントのカレンダー、予定、タスクリスト、タスクを同期します。';
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
@override
String get onboardingPreferencesStepTitle => 'システム設定を選択';
@@ -600,7 +600,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get removeAccountConfirmation =>
- 'このデバイスにキャッシュされたタスク、カレンダー、予定、リマインダー、保留中のオフライン変更が削除されます。同期されていない変更は失われます。Google または Microsoft から削除されるデータはありません。';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess => 'この Google アカウントへの BusyMax のアクセス権も取り消す';
@@ -619,7 +619,22 @@ class AppLocalizationsJa extends AppLocalizations {
'アカウントはこのデバイスから削除されましたが、BusyMax は Google アカウントへのアクセス権を取り消せませんでした。Google アカウントの設定から取り消すことができます。';
@override
- String get newList => '新しいリスト';
+ String get newTaskList => '新しいタスクリスト';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
@override
String get signInToViewTaskLists => 'タスクリストを表示するにはサインインしてください。';
@@ -642,6 +657,17 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get deleteList => 'リストを削除';
+ @override
+ String get unshare => '共有を解除';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
@override
String get builtInMicrosoftList => '組み込み';
@@ -654,6 +680,16 @@ class AppLocalizationsJa extends AppLocalizations {
return 'Google Tasks から「$title」を削除しますか?';
}
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
@override
String get deleteEvent => '予定を削除';
@@ -775,6 +811,74 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get doneStatus => '完了';
+ @override
+ String get taskStatus => 'Status';
+
+ @override
+ String get taskStatusNone => 'No status';
+
+ @override
+ String get taskStatusNeedsAction => 'アクションが必要';
+
+ @override
+ String get taskStatusInProcess => '進行中';
+
+ @override
+ String get taskStatusCompleted => '完了';
+
+ @override
+ String get taskStatusCancelled => 'Cancelled';
+
+ @override
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
+
+ @override
+ String get completionDate => 'Completion date';
+
+ @override
+ String get priority => '優先度';
+
+ @override
+ String get priorityNone => 'No priority';
+
+ @override
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
+
+ @override
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
+
+ @override
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
+
+ @override
+ String get taskUrl => 'URL';
+
+ @override
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
+
+ @override
+ String get classification => 'Classification';
+
+ @override
+ String get classificationPublic => 'When shared, show the full task';
+
+ @override
+ String get classificationConfidential => 'When shared, show only busy';
+
+ @override
+ String get classificationPrivate => 'When shared, hide this task';
+
+ @override
+ String get pinTask => 'Pin task';
+
@override
String get notes => 'メモ';
@@ -811,6 +915,84 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get addReminder => 'リマインダーを追加';
+ @override
+ String get reminders => 'Reminders';
+
+ @override
+ String get noReminders => 'No reminders';
+
+ @override
+ String get editReminder => 'Edit reminder';
+
+ @override
+ String get beforeTaskStarts => 'Before the task starts';
+
+ @override
+ String get beforeTaskDue => 'Before the task is due';
+
+ @override
+ String get afterTaskStarts => 'After the task starts';
+
+ @override
+ String get afterTaskDue => 'After the task is due';
+
+ @override
+ String get relativeToTaskStart => 'Relative to the task start date';
+
+ @override
+ String get relativeToTaskDue => 'Relative to the task due date';
+
+ @override
+ String get reminderTimeOfDay => 'Time of day';
+
+ @override
+ String get absoluteReminder => 'At a date and time';
+
+ @override
+ String get reminderAmount => 'Amount';
+
+ @override
+ String get reminderUnit => 'Unit';
+
+ @override
+ String get reminderUnitSeconds => 'Seconds';
+
+ @override
+ String get reminderUnitMinutes => 'Minutes';
+
+ @override
+ String get reminderUnitHours => 'Hours';
+
+ @override
+ String get reminderUnitDays => 'Days';
+
+ @override
+ String get reminderUnitWeeks => 'Weeks';
+
+ @override
+ String get reminderAtTaskStart => 'At the task start';
+
+ @override
+ String get reminderAtTaskDue => 'At the task due time';
+
+ @override
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
+
+ @override
+ String get relatedRemindersTitle => 'Keep related reminders?';
+
+ @override
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
+
+ @override
+ String get discardRelatedReminders => 'Discard reminders';
+
+ @override
+ String get keepRelatedReminders => 'Keep reminders';
+
@override
String get addGuest => 'ゲストを追加';
@@ -844,6 +1026,123 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get repeatYearly => '毎年';
+ @override
+ String get repeatEvery => '毎日繰り返す';
+
+ @override
+ String get repeatOn => 'Repeat on';
+
+ @override
+ String get repeatEnd => '繰り返し終了';
+
+ @override
+ String get repeatNever => 'Never';
+
+ @override
+ String get repeatUntil => 'On date';
+
+ @override
+ String get repeatAfter => 'After a number of occurrences';
+
+ @override
+ String get repeatCount => 'Occurrences';
+
+ @override
+ String get repeatDayOfMonth => 'Days of month';
+
+ @override
+ String get repeatMonths => 'Months';
+
+ @override
+ String get repeatOrdinal => 'Weekday position';
+
+ @override
+ String get repeatSpecificDays => 'Specific days';
+
+ @override
+ String get repeatFirst => 'First';
+
+ @override
+ String get repeatSecond => 'Second';
+
+ @override
+ String get repeatThird => 'Third';
+
+ @override
+ String get repeatFourth => 'Fourth';
+
+ @override
+ String get repeatFifth => 'Fifth';
+
+ @override
+ String get repeatSecondToLast => 'Second to last';
+
+ @override
+ String get repeatLast => 'Last';
+
+ @override
+ String get repeatAnyDay => 'Day';
+
+ @override
+ String get repeatWeekday => 'Weekday';
+
+ @override
+ String get repeatWeekendDay => 'Weekend day';
+
+ @override
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
+
+ @override
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
+
+ @override
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
+
+ @override
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
+
+ @override
+ String repeatOnDaysSummary(String days) {
+ return 'on $days';
+ }
+
+ @override
+ String repeatOnMonthDaysSummary(String days) {
+ return 'on day $days';
+ }
+
+ @override
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
+
+ @override
+ String repeatInMonthsSummary(String months) {
+ return 'in $months';
+ }
+
+ @override
+ String repeatTimesSummary(int count) {
+ return '$count回';
+ }
+
+ @override
+ String repeatUntilSummary(String date) {
+ return '$dateまで';
+ }
+
+ @override
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
+
@override
String get importance => '重要度';
@@ -893,6 +1192,26 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get createSubtask => 'サブタスクを作成';
+ @override
+ String get subtasks => 'サブタスク';
+
+ @override
+ String get duplicateTask => 'Duplicate task';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => 'サブタスクを非表示';
+
+ @override
+ String get hideClosedSubtasks => 'Hide closed subtasks';
+
@override
String get moveToTop => '一番上に移動';
@@ -904,7 +1223,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String deleteTaskConfirmation(String title) {
- return 'Google Tasks から「$title」を削除しますか?';
+ return '「$title」を削除しますか?';
}
@override
@@ -1219,6 +1538,222 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get noLocationsFound => '場所が見つかりません';
+ @override
+ String get requiredField => 'This field is required.';
+
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
+
+ @override
+ String get appleICloudProvider => 'Apple iCloud Calendar';
+
+ @override
+ String get nextcloudProvider => 'Nextcloud';
+
+ @override
+ String get appleICloudTasksProvider => 'Apple iCloud';
+
+ @override
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
+
+ @override
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
+
+ @override
+ String get addNextcloudAccount => 'Add Nextcloud account';
+
+ @override
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
+
+ @override
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
+
+ @override
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
+
+ @override
+ String get appleAccountEmail => 'Apple Account email';
+
+ @override
+ String get appleAppSpecificPassword => 'App-specific password';
+
+ @override
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
+
+ @override
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
+
+ @override
+ String get connectNextcloudTitle => 'Connect Nextcloud';
+
+ @override
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
+
+ @override
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
+
+ @override
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
+
+ @override
+ String get connectAccountAction => 'Connect';
+
+ @override
+ String get cancelAccountConnection => 'Cancel connection';
+
+ @override
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
+
+ @override
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
+
+ @override
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
+
+ @override
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
+
+ @override
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
+
+ @override
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
+
+ @override
+ String get collectionSettings => 'Calendars and task lists';
+
+ @override
+ String get calendarContent => 'Calendar events';
+
+ @override
+ String get taskContent => 'Tasks';
+
+ @override
+ String get readOnlySharedCollection => 'Read-only';
+
+ @override
+ String get pendingLocally => 'Pending locally';
+
+ @override
+ String get conflictBlocked => 'Blocked by conflict';
+
+ @override
+ String get authenticationBlocked => 'Blocked until reconnect';
+
+ @override
+ String get operationFailed => 'Operation failed';
+
+ @override
+ String get keepServerVersion => 'Keep server version';
+
+ @override
+ String get reapplyLocalChange => 'Review and reapply local change';
+
+ @override
+ String get duplicateLocalItem => 'Duplicate as new item';
+
+ @override
+ String get davConnectionState => 'Connection state';
+
+ @override
+ String get davConnected => 'Connected';
+
+ @override
+ String get davConnecting => 'Connecting…';
+
+ @override
+ String get davSignedOut => 'Signed out';
+
+ @override
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
+
+ @override
+ String get thisAndFutureUnavailable => 'This and future (not available)';
+
+ @override
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
+
+ @override
+ String get taskDueBeforeStart => '期限を開始より前に設定することはできません。';
+
+ @override
+ String get taskStartDueTimeModeMismatch => '開始と期限の両方に時刻を設定するか、タスクを終日にしてください。';
+
@override
String deleteCalendarConfirmation(String title) {
return '「$title」を削除しますか?';
diff --git a/lib/l10n/generated/app_localizations_ko.dart b/lib/l10n/generated/app_localizations_ko.dart
index dee60a8..eab5907 100644
--- a/lib/l10n/generated/app_localizations_ko.dart
+++ b/lib/l10n/generated/app_localizations_ko.dart
@@ -17,7 +17,7 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get connectGoogleAccount =>
- 'Google 및 Microsoft 계정을 연결하여 캘린더와 할 일을 동기화하세요.';
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
@override
String get googlePermissionsConsentNotice =>
@@ -41,7 +41,7 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get onboardingAccountsStepDescription =>
- '사용할 Google 및 Microsoft 계정을 모두 추가하세요. BusyMax는 각 계정의 캘린더, 일정, 할 일 목록 및 할 일을 동기화합니다.';
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
@override
String get onboardingPreferencesStepTitle => '시스템 설정 선택';
@@ -600,7 +600,7 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get removeAccountConfirmation =>
- '이 기기에서 캐시된 할 일, 캘린더, 일정, 미리 알림 및 보류 중인 오프라인 변경 사항이 삭제됩니다. 동기화되지 않은 변경 사항은 사라집니다. Google 또는 Microsoft에서는 아무것도 삭제되지 않습니다.';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess => '이 Google 계정에 대한 BusyMax의 액세스 권한도 취소';
@@ -619,7 +619,22 @@ class AppLocalizationsKo extends AppLocalizations {
'계정은 이 기기에서 삭제되었지만 BusyMax가 Google 계정 액세스 권한을 취소하지 못했습니다. Google 계정에서 직접 취소할 수 있습니다.';
@override
- String get newList => '새 목록';
+ String get newTaskList => '새 할 일 목록';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
@override
String get signInToViewTaskLists => '할 일 목록을 보려면 로그인하세요.';
@@ -642,6 +657,17 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get deleteList => '목록 삭제';
+ @override
+ String get unshare => '공유 해제';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
@override
String get builtInMicrosoftList => '기본 제공';
@@ -654,6 +680,16 @@ class AppLocalizationsKo extends AppLocalizations {
return 'Google Tasks에서 “$title” 목록을 삭제할까요?';
}
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
@override
String get deleteEvent => '일정 삭제';
@@ -775,6 +811,74 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get doneStatus => '완료';
+ @override
+ String get taskStatus => 'Status';
+
+ @override
+ String get taskStatusNone => 'No status';
+
+ @override
+ String get taskStatusNeedsAction => 'Needs action';
+
+ @override
+ String get taskStatusInProcess => 'In process';
+
+ @override
+ String get taskStatusCompleted => '완료됨';
+
+ @override
+ String get taskStatusCancelled => 'Cancelled';
+
+ @override
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
+
+ @override
+ String get completionDate => 'Completion date';
+
+ @override
+ String get priority => '우선 순위';
+
+ @override
+ String get priorityNone => 'No priority';
+
+ @override
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
+
+ @override
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
+
+ @override
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
+
+ @override
+ String get taskUrl => 'URL';
+
+ @override
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
+
+ @override
+ String get classification => 'Classification';
+
+ @override
+ String get classificationPublic => 'When shared, show the full task';
+
+ @override
+ String get classificationConfidential => 'When shared, show only busy';
+
+ @override
+ String get classificationPrivate => 'When shared, hide this task';
+
+ @override
+ String get pinTask => 'Pin task';
+
@override
String get notes => '메모';
@@ -811,6 +915,84 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get addReminder => '미리 알림 추가';
+ @override
+ String get reminders => 'Reminders';
+
+ @override
+ String get noReminders => 'No reminders';
+
+ @override
+ String get editReminder => 'Edit reminder';
+
+ @override
+ String get beforeTaskStarts => 'Before the task starts';
+
+ @override
+ String get beforeTaskDue => 'Before the task is due';
+
+ @override
+ String get afterTaskStarts => 'After the task starts';
+
+ @override
+ String get afterTaskDue => 'After the task is due';
+
+ @override
+ String get relativeToTaskStart => 'Relative to the task start date';
+
+ @override
+ String get relativeToTaskDue => 'Relative to the task due date';
+
+ @override
+ String get reminderTimeOfDay => 'Time of day';
+
+ @override
+ String get absoluteReminder => 'At a date and time';
+
+ @override
+ String get reminderAmount => 'Amount';
+
+ @override
+ String get reminderUnit => 'Unit';
+
+ @override
+ String get reminderUnitSeconds => 'Seconds';
+
+ @override
+ String get reminderUnitMinutes => 'Minutes';
+
+ @override
+ String get reminderUnitHours => 'Hours';
+
+ @override
+ String get reminderUnitDays => 'Days';
+
+ @override
+ String get reminderUnitWeeks => 'Weeks';
+
+ @override
+ String get reminderAtTaskStart => 'At the task start';
+
+ @override
+ String get reminderAtTaskDue => 'At the task due time';
+
+ @override
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
+
+ @override
+ String get relatedRemindersTitle => 'Keep related reminders?';
+
+ @override
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
+
+ @override
+ String get discardRelatedReminders => 'Discard reminders';
+
+ @override
+ String get keepRelatedReminders => 'Keep reminders';
+
@override
String get addGuest => '참석자 추가';
@@ -844,6 +1026,123 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get repeatYearly => '매년';
+ @override
+ String get repeatEvery => '반복 주기';
+
+ @override
+ String get repeatOn => 'Repeat on';
+
+ @override
+ String get repeatEnd => '반복 종료';
+
+ @override
+ String get repeatNever => 'Never';
+
+ @override
+ String get repeatUntil => 'On date';
+
+ @override
+ String get repeatAfter => 'After a number of occurrences';
+
+ @override
+ String get repeatCount => 'Occurrences';
+
+ @override
+ String get repeatDayOfMonth => 'Days of month';
+
+ @override
+ String get repeatMonths => 'Months';
+
+ @override
+ String get repeatOrdinal => 'Weekday position';
+
+ @override
+ String get repeatSpecificDays => 'Specific days';
+
+ @override
+ String get repeatFirst => 'First';
+
+ @override
+ String get repeatSecond => 'Second';
+
+ @override
+ String get repeatThird => 'Third';
+
+ @override
+ String get repeatFourth => 'Fourth';
+
+ @override
+ String get repeatFifth => 'Fifth';
+
+ @override
+ String get repeatSecondToLast => 'Second to last';
+
+ @override
+ String get repeatLast => 'Last';
+
+ @override
+ String get repeatAnyDay => 'Day';
+
+ @override
+ String get repeatWeekday => 'Weekday';
+
+ @override
+ String get repeatWeekendDay => 'Weekend day';
+
+ @override
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
+
+ @override
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
+
+ @override
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
+
+ @override
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
+
+ @override
+ String repeatOnDaysSummary(String days) {
+ return 'on $days';
+ }
+
+ @override
+ String repeatOnMonthDaysSummary(String days) {
+ return 'on day $days';
+ }
+
+ @override
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
+
+ @override
+ String repeatInMonthsSummary(String months) {
+ return 'in $months';
+ }
+
+ @override
+ String repeatTimesSummary(int count) {
+ return '$count회';
+ }
+
+ @override
+ String repeatUntilSummary(String date) {
+ return 'until $date';
+ }
+
+ @override
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
+
@override
String get importance => '중요도';
@@ -893,6 +1192,26 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get createSubtask => '하위 할 일 만들기';
+ @override
+ String get subtasks => '하위 할 일';
+
+ @override
+ String get duplicateTask => 'Duplicate task';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => 'Hide subtasks';
+
+ @override
+ String get hideClosedSubtasks => 'Hide closed subtasks';
+
@override
String get moveToTop => '맨 위로 이동';
@@ -904,7 +1223,7 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String deleteTaskConfirmation(String title) {
- return 'Google Tasks에서 “$title” 항목을 삭제할까요?';
+ return '“$title” 항목을 삭제할까요?';
}
@override
@@ -1219,6 +1538,223 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get noLocationsFound => '위치를 찾을 수 없습니다';
+ @override
+ String get requiredField => 'This field is required.';
+
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
+
+ @override
+ String get appleICloudProvider => 'Apple iCloud Calendar';
+
+ @override
+ String get nextcloudProvider => 'Nextcloud';
+
+ @override
+ String get appleICloudTasksProvider => 'Apple iCloud';
+
+ @override
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
+
+ @override
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
+
+ @override
+ String get addNextcloudAccount => 'Add Nextcloud account';
+
+ @override
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
+
+ @override
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
+
+ @override
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
+
+ @override
+ String get appleAccountEmail => 'Apple Account email';
+
+ @override
+ String get appleAppSpecificPassword => 'App-specific password';
+
+ @override
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
+
+ @override
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
+
+ @override
+ String get connectNextcloudTitle => 'Connect Nextcloud';
+
+ @override
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
+
+ @override
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
+
+ @override
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
+
+ @override
+ String get connectAccountAction => 'Connect';
+
+ @override
+ String get cancelAccountConnection => 'Cancel connection';
+
+ @override
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
+
+ @override
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
+
+ @override
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
+
+ @override
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
+
+ @override
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
+
+ @override
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
+
+ @override
+ String get collectionSettings => 'Calendars and task lists';
+
+ @override
+ String get calendarContent => 'Calendar events';
+
+ @override
+ String get taskContent => 'Tasks';
+
+ @override
+ String get readOnlySharedCollection => 'Read-only';
+
+ @override
+ String get pendingLocally => 'Pending locally';
+
+ @override
+ String get conflictBlocked => 'Blocked by conflict';
+
+ @override
+ String get authenticationBlocked => 'Blocked until reconnect';
+
+ @override
+ String get operationFailed => 'Operation failed';
+
+ @override
+ String get keepServerVersion => 'Keep server version';
+
+ @override
+ String get reapplyLocalChange => 'Review and reapply local change';
+
+ @override
+ String get duplicateLocalItem => 'Duplicate as new item';
+
+ @override
+ String get davConnectionState => 'Connection state';
+
+ @override
+ String get davConnected => 'Connected';
+
+ @override
+ String get davConnecting => 'Connecting…';
+
+ @override
+ String get davSignedOut => 'Signed out';
+
+ @override
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
+
+ @override
+ String get thisAndFutureUnavailable => 'This and future (not available)';
+
+ @override
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
+
+ @override
+ String get taskDueBeforeStart => '마감은 시작보다 빠를 수 없습니다.';
+
+ @override
+ String get taskStartDueTimeModeMismatch =>
+ '시작과 마감에 모두 시간을 설정하거나 작업을 종일로 설정하세요.';
+
@override
String deleteCalendarConfirmation(String title) {
return '“$title” 캘린더를 삭제할까요?';
diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart
index 14bbdea..08918c0 100644
--- a/lib/l10n/generated/app_localizations_pt.dart
+++ b/lib/l10n/generated/app_localizations_pt.dart
@@ -17,7 +17,7 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get connectGoogleAccount =>
- 'Ligue contas Google e Microsoft para sincronizar calendários e tarefas.';
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
@override
String get googlePermissionsConsentNotice =>
@@ -41,7 +41,7 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get onboardingAccountsStepDescription =>
- 'Adicione todas as contas Google e Microsoft que pretende utilizar. O BusyMax sincroniza calendários, eventos, listas de tarefas e tarefas de cada conta.';
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
@override
String get onboardingPreferencesStepTitle => 'Escolher definições do sistema';
@@ -615,7 +615,7 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get removeAccountConfirmation =>
- 'Esta ação elimina deste dispositivo as tarefas, os calendários, os eventos, os lembretes e as alterações offline pendentes em cache. As alterações não sincronizadas serão perdidas. Nada será eliminado da Google ou da Microsoft.';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess =>
@@ -637,7 +637,22 @@ class AppLocalizationsPt extends AppLocalizations {
'A conta foi removida deste dispositivo, mas não foi possível revogar o acesso do BusyMax à sua conta Google. Pode revogar esse acesso na sua conta Google.';
@override
- String get newList => 'Nova lista';
+ String get newTaskList => 'Nova lista de tarefas';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
@override
String get signInToViewTaskLists =>
@@ -662,6 +677,17 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get deleteList => 'Eliminar lista';
+ @override
+ String get unshare => 'Cancelar partilha';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
@override
String get builtInMicrosoftList => 'Incorporada';
@@ -674,6 +700,16 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Eliminar «$title» do Google Tasks?';
}
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
@override
String get deleteEvent => 'Eliminar evento';
@@ -798,6 +834,74 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get doneStatus => 'Concluída';
+ @override
+ String get taskStatus => 'Status';
+
+ @override
+ String get taskStatusNone => 'No status';
+
+ @override
+ String get taskStatusNeedsAction => 'Needs action';
+
+ @override
+ String get taskStatusInProcess => 'Em andamento';
+
+ @override
+ String get taskStatusCompleted => 'Concluída';
+
+ @override
+ String get taskStatusCancelled => 'Cancelled';
+
+ @override
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
+
+ @override
+ String get completionDate => 'Completion date';
+
+ @override
+ String get priority => 'Prioridade';
+
+ @override
+ String get priorityNone => 'No priority';
+
+ @override
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
+
+ @override
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
+
+ @override
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
+
+ @override
+ String get taskUrl => 'URL';
+
+ @override
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
+
+ @override
+ String get classification => 'Classification';
+
+ @override
+ String get classificationPublic => 'When shared, show the full task';
+
+ @override
+ String get classificationConfidential => 'When shared, show only busy';
+
+ @override
+ String get classificationPrivate => 'When shared, hide this task';
+
+ @override
+ String get pinTask => 'Pin task';
+
@override
String get notes => 'Notas';
@@ -834,6 +938,84 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get addReminder => 'Adicionar lembrete';
+ @override
+ String get reminders => 'Reminders';
+
+ @override
+ String get noReminders => 'No reminders';
+
+ @override
+ String get editReminder => 'Edit reminder';
+
+ @override
+ String get beforeTaskStarts => 'Before the task starts';
+
+ @override
+ String get beforeTaskDue => 'Before the task is due';
+
+ @override
+ String get afterTaskStarts => 'After the task starts';
+
+ @override
+ String get afterTaskDue => 'After the task is due';
+
+ @override
+ String get relativeToTaskStart => 'Relative to the task start date';
+
+ @override
+ String get relativeToTaskDue => 'Relative to the task due date';
+
+ @override
+ String get reminderTimeOfDay => 'Time of day';
+
+ @override
+ String get absoluteReminder => 'At a date and time';
+
+ @override
+ String get reminderAmount => 'Amount';
+
+ @override
+ String get reminderUnit => 'Unit';
+
+ @override
+ String get reminderUnitSeconds => 'Seconds';
+
+ @override
+ String get reminderUnitMinutes => 'Minutes';
+
+ @override
+ String get reminderUnitHours => 'Hours';
+
+ @override
+ String get reminderUnitDays => 'Days';
+
+ @override
+ String get reminderUnitWeeks => 'Weeks';
+
+ @override
+ String get reminderAtTaskStart => 'At the task start';
+
+ @override
+ String get reminderAtTaskDue => 'At the task due time';
+
+ @override
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
+
+ @override
+ String get relatedRemindersTitle => 'Keep related reminders?';
+
+ @override
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
+
+ @override
+ String get discardRelatedReminders => 'Discard reminders';
+
+ @override
+ String get keepRelatedReminders => 'Keep reminders';
+
@override
String get addGuest => 'Adicionar convidado';
@@ -867,6 +1049,123 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get repeatYearly => 'Anualmente';
+ @override
+ String get repeatEvery => 'Repetir a cada';
+
+ @override
+ String get repeatOn => 'Repeat on';
+
+ @override
+ String get repeatEnd => 'End repeat';
+
+ @override
+ String get repeatNever => 'Never';
+
+ @override
+ String get repeatUntil => 'On date';
+
+ @override
+ String get repeatAfter => 'After a number of occurrences';
+
+ @override
+ String get repeatCount => 'Occurrences';
+
+ @override
+ String get repeatDayOfMonth => 'Days of month';
+
+ @override
+ String get repeatMonths => 'Months';
+
+ @override
+ String get repeatOrdinal => 'Weekday position';
+
+ @override
+ String get repeatSpecificDays => 'Specific days';
+
+ @override
+ String get repeatFirst => 'First';
+
+ @override
+ String get repeatSecond => 'Second';
+
+ @override
+ String get repeatThird => 'Third';
+
+ @override
+ String get repeatFourth => 'Fourth';
+
+ @override
+ String get repeatFifth => 'Fifth';
+
+ @override
+ String get repeatSecondToLast => 'Second to last';
+
+ @override
+ String get repeatLast => 'Last';
+
+ @override
+ String get repeatAnyDay => 'Day';
+
+ @override
+ String get repeatWeekday => 'Weekday';
+
+ @override
+ String get repeatWeekendDay => 'Weekend day';
+
+ @override
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
+
+ @override
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
+
+ @override
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
+
+ @override
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
+
+ @override
+ String repeatOnDaysSummary(String days) {
+ return 'em $days';
+ }
+
+ @override
+ String repeatOnMonthDaysSummary(String days) {
+ return 'on day $days';
+ }
+
+ @override
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
+
+ @override
+ String repeatInMonthsSummary(String months) {
+ return 'in $months';
+ }
+
+ @override
+ String repeatTimesSummary(int count) {
+ return '$count vezes';
+ }
+
+ @override
+ String repeatUntilSummary(String date) {
+ return 'até $date';
+ }
+
+ @override
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
+
@override
String get importance => 'Importância';
@@ -916,6 +1215,26 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get createSubtask => 'Criar subtarefa';
+ @override
+ String get subtasks => 'Subtarefas';
+
+ @override
+ String get duplicateTask => 'Duplicate task';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => 'Ocultar subtarefas';
+
+ @override
+ String get hideClosedSubtasks => 'Hide closed subtasks';
+
@override
String get moveToTop => 'Mover para o início';
@@ -927,7 +1246,7 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String deleteTaskConfirmation(String title) {
- return 'Eliminar «$title» do Google Tasks?';
+ return 'Eliminar «$title»?';
}
@override
@@ -1253,6 +1572,223 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get noLocationsFound => 'Nenhum local encontrado';
+ @override
+ String get requiredField => 'This field is required.';
+
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
+
+ @override
+ String get appleICloudProvider => 'Apple iCloud Calendar';
+
+ @override
+ String get nextcloudProvider => 'Nextcloud';
+
+ @override
+ String get appleICloudTasksProvider => 'Apple iCloud';
+
+ @override
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
+
+ @override
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
+
+ @override
+ String get addNextcloudAccount => 'Add Nextcloud account';
+
+ @override
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
+
+ @override
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
+
+ @override
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
+
+ @override
+ String get appleAccountEmail => 'Apple Account email';
+
+ @override
+ String get appleAppSpecificPassword => 'App-specific password';
+
+ @override
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
+
+ @override
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
+
+ @override
+ String get connectNextcloudTitle => 'Connect Nextcloud';
+
+ @override
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
+
+ @override
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
+
+ @override
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
+
+ @override
+ String get connectAccountAction => 'Connect';
+
+ @override
+ String get cancelAccountConnection => 'Cancel connection';
+
+ @override
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
+
+ @override
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
+
+ @override
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
+
+ @override
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
+
+ @override
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
+
+ @override
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
+
+ @override
+ String get collectionSettings => 'Calendars and task lists';
+
+ @override
+ String get calendarContent => 'Calendar events';
+
+ @override
+ String get taskContent => 'Tasks';
+
+ @override
+ String get readOnlySharedCollection => 'Read-only';
+
+ @override
+ String get pendingLocally => 'Pending locally';
+
+ @override
+ String get conflictBlocked => 'Blocked by conflict';
+
+ @override
+ String get authenticationBlocked => 'Blocked until reconnect';
+
+ @override
+ String get operationFailed => 'Operation failed';
+
+ @override
+ String get keepServerVersion => 'Keep server version';
+
+ @override
+ String get reapplyLocalChange => 'Review and reapply local change';
+
+ @override
+ String get duplicateLocalItem => 'Duplicate as new item';
+
+ @override
+ String get davConnectionState => 'Connection state';
+
+ @override
+ String get davConnected => 'Connected';
+
+ @override
+ String get davConnecting => 'Connecting…';
+
+ @override
+ String get davSignedOut => 'Signed out';
+
+ @override
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
+
+ @override
+ String get thisAndFutureUnavailable => 'This and future (not available)';
+
+ @override
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
+
+ @override
+ String get taskDueBeforeStart => 'O prazo não pode ser anterior ao início.';
+
+ @override
+ String get taskStartDueTimeModeMismatch =>
+ 'Defina horários para o início e o prazo, ou torne a tarefa de dia inteiro.';
+
@override
String deleteCalendarConfirmation(String title) {
return 'Eliminar «$title»?';
diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart
index 728ec35..12b1ee3 100644
--- a/lib/l10n/generated/app_localizations_ru.dart
+++ b/lib/l10n/generated/app_localizations_ru.dart
@@ -17,7 +17,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get connectGoogleAccount =>
- 'Подключите аккаунты Google и Microsoft, чтобы синхронизировать календари и задачи.';
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
@override
String get googlePermissionsConsentNotice =>
@@ -41,7 +41,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get onboardingAccountsStepDescription =>
- 'Добавьте все аккаунты Google и Microsoft, которые хотите использовать. BusyMax синхронизирует календари, события, списки задач и задачи из каждого аккаунта.';
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
@override
String get onboardingPreferencesStepTitle => 'Выберите системные параметры';
@@ -618,7 +618,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get removeAccountConfirmation =>
- 'С этого устройства будут удалены кэшированные задачи, календари, события, напоминания и локальные изменения, ожидающие синхронизации. Несинхронизированные изменения будут потеряны. В Google и Microsoft ничего не будет удалено.';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess =>
@@ -640,7 +640,22 @@ class AppLocalizationsRu extends AppLocalizations {
'Аккаунт удалён с этого устройства, но отозвать доступ BusyMax к Google не удалось. Вы можете отозвать доступ в аккаунте Google.';
@override
- String get newList => 'Новый список';
+ String get newTaskList => 'Новый список задач';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
@override
String get signInToViewTaskLists =>
@@ -664,6 +679,17 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get deleteList => 'Удалить список';
+ @override
+ String get unshare => 'Закрыть доступ';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
@override
String get builtInMicrosoftList => 'Встроенный';
@@ -676,6 +702,16 @@ class AppLocalizationsRu extends AppLocalizations {
return 'Удалить «$title» из Google Tasks?';
}
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
@override
String get deleteEvent => 'Удалить событие';
@@ -800,6 +836,74 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get doneStatus => 'Выполнена';
+ @override
+ String get taskStatus => 'Status';
+
+ @override
+ String get taskStatusNone => 'No status';
+
+ @override
+ String get taskStatusNeedsAction => 'Требуется действие';
+
+ @override
+ String get taskStatusInProcess => 'Выполянется';
+
+ @override
+ String get taskStatusCompleted => 'Завершённые';
+
+ @override
+ String get taskStatusCancelled => 'Cancelled';
+
+ @override
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
+
+ @override
+ String get completionDate => 'Completion date';
+
+ @override
+ String get priority => 'Приоритет';
+
+ @override
+ String get priorityNone => 'No priority';
+
+ @override
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
+
+ @override
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
+
+ @override
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
+
+ @override
+ String get taskUrl => 'URL';
+
+ @override
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
+
+ @override
+ String get classification => 'Classification';
+
+ @override
+ String get classificationPublic => 'When shared, show the full task';
+
+ @override
+ String get classificationConfidential => 'When shared, show only busy';
+
+ @override
+ String get classificationPrivate => 'When shared, hide this task';
+
+ @override
+ String get pinTask => 'Pin task';
+
@override
String get notes => 'Заметки';
@@ -836,6 +940,84 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get addReminder => 'Добавить напоминание';
+ @override
+ String get reminders => 'Reminders';
+
+ @override
+ String get noReminders => 'Напоминаний нет';
+
+ @override
+ String get editReminder => 'Edit reminder';
+
+ @override
+ String get beforeTaskStarts => 'До начала задачи';
+
+ @override
+ String get beforeTaskDue => 'До срока выполнения задачи';
+
+ @override
+ String get afterTaskStarts => 'After the task starts';
+
+ @override
+ String get afterTaskDue => 'After the task is due';
+
+ @override
+ String get relativeToTaskStart => 'Relative to the task start date';
+
+ @override
+ String get relativeToTaskDue => 'Relative to the task due date';
+
+ @override
+ String get reminderTimeOfDay => 'Time of day';
+
+ @override
+ String get absoluteReminder => 'At a date and time';
+
+ @override
+ String get reminderAmount => 'Amount';
+
+ @override
+ String get reminderUnit => 'Unit';
+
+ @override
+ String get reminderUnitSeconds => 'Seconds';
+
+ @override
+ String get reminderUnitMinutes => 'Minutes';
+
+ @override
+ String get reminderUnitHours => 'Hours';
+
+ @override
+ String get reminderUnitDays => 'Days';
+
+ @override
+ String get reminderUnitWeeks => 'Weeks';
+
+ @override
+ String get reminderAtTaskStart => 'At the task start';
+
+ @override
+ String get reminderAtTaskDue => 'At the task due time';
+
+ @override
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
+
+ @override
+ String get relatedRemindersTitle => 'Keep related reminders?';
+
+ @override
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
+
+ @override
+ String get discardRelatedReminders => 'Отменить напоминания';
+
+ @override
+ String get keepRelatedReminders => 'Сохранить напоминания';
+
@override
String get addGuest => 'Добавить гостя';
@@ -869,6 +1051,123 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get repeatYearly => 'Ежегодно';
+ @override
+ String get repeatEvery => 'Повторять каждые';
+
+ @override
+ String get repeatOn => 'Repeat on';
+
+ @override
+ String get repeatEnd => 'Прекратить повтор';
+
+ @override
+ String get repeatNever => 'Never';
+
+ @override
+ String get repeatUntil => 'On date';
+
+ @override
+ String get repeatAfter => 'After a number of occurrences';
+
+ @override
+ String get repeatCount => 'Occurrences';
+
+ @override
+ String get repeatDayOfMonth => 'Days of month';
+
+ @override
+ String get repeatMonths => 'Months';
+
+ @override
+ String get repeatOrdinal => 'Weekday position';
+
+ @override
+ String get repeatSpecificDays => 'Specific days';
+
+ @override
+ String get repeatFirst => 'First';
+
+ @override
+ String get repeatSecond => 'Second';
+
+ @override
+ String get repeatThird => 'Third';
+
+ @override
+ String get repeatFourth => 'Fourth';
+
+ @override
+ String get repeatFifth => 'Fifth';
+
+ @override
+ String get repeatSecondToLast => 'Second to last';
+
+ @override
+ String get repeatLast => 'Last';
+
+ @override
+ String get repeatAnyDay => 'Day';
+
+ @override
+ String get repeatWeekday => 'Weekday';
+
+ @override
+ String get repeatWeekendDay => 'Weekend day';
+
+ @override
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
+
+ @override
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
+
+ @override
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
+
+ @override
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
+
+ @override
+ String repeatOnDaysSummary(String days) {
+ return 'в $days';
+ }
+
+ @override
+ String repeatOnMonthDaysSummary(String days) {
+ return ' $days';
+ }
+
+ @override
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
+
+ @override
+ String repeatInMonthsSummary(String months) {
+ return 'в $months';
+ }
+
+ @override
+ String repeatTimesSummary(int count) {
+ return '$count раз';
+ }
+
+ @override
+ String repeatUntilSummary(String date) {
+ return 'до $date';
+ }
+
+ @override
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
+
@override
String get importance => 'Важность';
@@ -918,6 +1217,26 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get createSubtask => 'Создать подзадачу';
+ @override
+ String get subtasks => 'Подзадачи';
+
+ @override
+ String get duplicateTask => 'Дублировать задачу';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => 'Скрыть вложенные задачи';
+
+ @override
+ String get hideClosedSubtasks => 'Скрыть закрытые подзадачи';
+
@override
String get moveToTop => 'Переместить в самый верх';
@@ -929,7 +1248,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String deleteTaskConfirmation(String title) {
- return 'Удалить «$title» из Google Tasks?';
+ return 'Удалить «$title»?';
}
@override
@@ -1256,6 +1575,223 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get noLocationsFound => 'Ничего не найдено';
+ @override
+ String get requiredField => 'This field is required.';
+
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
+
+ @override
+ String get appleICloudProvider => 'Apple iCloud Calendar';
+
+ @override
+ String get nextcloudProvider => 'Nextcloud';
+
+ @override
+ String get appleICloudTasksProvider => 'Apple iCloud';
+
+ @override
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
+
+ @override
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
+
+ @override
+ String get addNextcloudAccount => 'Add Nextcloud account';
+
+ @override
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
+
+ @override
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
+
+ @override
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
+
+ @override
+ String get appleAccountEmail => 'Apple Account email';
+
+ @override
+ String get appleAppSpecificPassword => 'App-specific password';
+
+ @override
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
+
+ @override
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
+
+ @override
+ String get connectNextcloudTitle => 'Connect Nextcloud';
+
+ @override
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
+
+ @override
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
+
+ @override
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
+
+ @override
+ String get connectAccountAction => 'Connect';
+
+ @override
+ String get cancelAccountConnection => 'Cancel connection';
+
+ @override
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
+
+ @override
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
+
+ @override
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
+
+ @override
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
+
+ @override
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
+
+ @override
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
+
+ @override
+ String get collectionSettings => 'Calendars and task lists';
+
+ @override
+ String get calendarContent => 'Calendar events';
+
+ @override
+ String get taskContent => 'Tasks';
+
+ @override
+ String get readOnlySharedCollection => 'Read-only';
+
+ @override
+ String get pendingLocally => 'Pending locally';
+
+ @override
+ String get conflictBlocked => 'Blocked by conflict';
+
+ @override
+ String get authenticationBlocked => 'Blocked until reconnect';
+
+ @override
+ String get operationFailed => 'Operation failed';
+
+ @override
+ String get keepServerVersion => 'Keep server version';
+
+ @override
+ String get reapplyLocalChange => 'Review and reapply local change';
+
+ @override
+ String get duplicateLocalItem => 'Duplicate as new item';
+
+ @override
+ String get davConnectionState => 'Connection state';
+
+ @override
+ String get davConnected => 'Connected';
+
+ @override
+ String get davConnecting => 'Connecting…';
+
+ @override
+ String get davSignedOut => 'Signed out';
+
+ @override
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
+
+ @override
+ String get thisAndFutureUnavailable => 'This and future (not available)';
+
+ @override
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
+
+ @override
+ String get taskDueBeforeStart => 'Срок не может быть раньше начала.';
+
+ @override
+ String get taskStartDueTimeModeMismatch =>
+ 'Укажите время начала и срока или сделайте задачу на весь день.';
+
@override
String deleteCalendarConfirmation(String title) {
return 'Удалить «$title»?';
diff --git a/lib/l10n/generated/app_localizations_vi.dart b/lib/l10n/generated/app_localizations_vi.dart
index 0abb69c..c37e912 100644
--- a/lib/l10n/generated/app_localizations_vi.dart
+++ b/lib/l10n/generated/app_localizations_vi.dart
@@ -17,7 +17,7 @@ class AppLocalizationsVi extends AppLocalizations {
@override
String get connectGoogleAccount =>
- 'Kết nối tài khoản Google và Microsoft để đồng bộ lịch và công việc.';
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
@override
String get googlePermissionsConsentNotice =>
@@ -41,7 +41,7 @@ class AppLocalizationsVi extends AppLocalizations {
@override
String get onboardingAccountsStepDescription =>
- 'Thêm tất cả tài khoản Google và Microsoft bạn muốn sử dụng. BusyMax đồng bộ lịch, sự kiện, danh sách công việc và công việc từ mỗi tài khoản.';
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
@override
String get onboardingPreferencesStepTitle => 'Chọn cài đặt hệ thống';
@@ -612,7 +612,7 @@ class AppLocalizationsVi extends AppLocalizations {
@override
String get removeAccountConfirmation =>
- 'Thao tác này sẽ xóa công việc, lịch, sự kiện, lời nhắc đã lưu trong bộ nhớ đệm và các thay đổi ngoại tuyến đang chờ khỏi thiết bị. Các thay đổi chưa đồng bộ sẽ bị mất. Không có dữ liệu nào bị xóa khỏi Google hoặc Microsoft.';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess =>
@@ -634,7 +634,22 @@ class AppLocalizationsVi extends AppLocalizations {
'Tài khoản đã bị xóa khỏi thiết bị này, nhưng BusyMax không thể thu hồi quyền truy cập vào tài khoản Google. Bạn có thể thu hồi quyền trong phần cài đặt Tài khoản Google.';
@override
- String get newList => 'Danh sách mới';
+ String get newTaskList => 'Danh sách công việc mới';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
@override
String get signInToViewTaskLists => 'Đăng nhập để xem danh sách công việc.';
@@ -658,6 +673,17 @@ class AppLocalizationsVi extends AppLocalizations {
@override
String get deleteList => 'Xóa danh sách';
+ @override
+ String get unshare => 'Bỏ chia sẽ';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
@override
String get builtInMicrosoftList => 'Tích hợp sẵn';
@@ -670,6 +696,16 @@ class AppLocalizationsVi extends AppLocalizations {
return 'Xóa “$title” khỏi Google Tasks?';
}
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
@override
String get deleteEvent => 'Xóa sự kiện';
@@ -794,6 +830,74 @@ class AppLocalizationsVi extends AppLocalizations {
@override
String get doneStatus => 'Đã hoàn thành';
+ @override
+ String get taskStatus => 'Status';
+
+ @override
+ String get taskStatusNone => 'No status';
+
+ @override
+ String get taskStatusNeedsAction => 'Needs action';
+
+ @override
+ String get taskStatusInProcess => 'In process';
+
+ @override
+ String get taskStatusCompleted => 'Hoàn thành';
+
+ @override
+ String get taskStatusCancelled => 'Cancelled';
+
+ @override
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
+
+ @override
+ String get completionDate => 'Completion date';
+
+ @override
+ String get priority => 'Priority';
+
+ @override
+ String get priorityNone => 'No priority';
+
+ @override
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
+
+ @override
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
+
+ @override
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
+
+ @override
+ String get taskUrl => 'URL';
+
+ @override
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
+
+ @override
+ String get classification => 'Classification';
+
+ @override
+ String get classificationPublic => 'When shared, show the full task';
+
+ @override
+ String get classificationConfidential => 'When shared, show only busy';
+
+ @override
+ String get classificationPrivate => 'When shared, hide this task';
+
+ @override
+ String get pinTask => 'Pin task';
+
@override
String get notes => 'Ghi chú';
@@ -830,6 +934,84 @@ class AppLocalizationsVi extends AppLocalizations {
@override
String get addReminder => 'Thêm lời nhắc';
+ @override
+ String get reminders => 'Reminders';
+
+ @override
+ String get noReminders => 'No reminders';
+
+ @override
+ String get editReminder => 'Edit reminder';
+
+ @override
+ String get beforeTaskStarts => 'Before the task starts';
+
+ @override
+ String get beforeTaskDue => 'Before the task is due';
+
+ @override
+ String get afterTaskStarts => 'After the task starts';
+
+ @override
+ String get afterTaskDue => 'After the task is due';
+
+ @override
+ String get relativeToTaskStart => 'Relative to the task start date';
+
+ @override
+ String get relativeToTaskDue => 'Relative to the task due date';
+
+ @override
+ String get reminderTimeOfDay => 'Time of day';
+
+ @override
+ String get absoluteReminder => 'At a date and time';
+
+ @override
+ String get reminderAmount => 'Amount';
+
+ @override
+ String get reminderUnit => 'Unit';
+
+ @override
+ String get reminderUnitSeconds => 'Seconds';
+
+ @override
+ String get reminderUnitMinutes => 'Minutes';
+
+ @override
+ String get reminderUnitHours => 'Hours';
+
+ @override
+ String get reminderUnitDays => 'Days';
+
+ @override
+ String get reminderUnitWeeks => 'Weeks';
+
+ @override
+ String get reminderAtTaskStart => 'At the task start';
+
+ @override
+ String get reminderAtTaskDue => 'At the task due time';
+
+ @override
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
+
+ @override
+ String get relatedRemindersTitle => 'Keep related reminders?';
+
+ @override
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
+
+ @override
+ String get discardRelatedReminders => 'Discard reminders';
+
+ @override
+ String get keepRelatedReminders => 'Keep reminders';
+
@override
String get addGuest => 'Thêm khách mời';
@@ -863,6 +1045,123 @@ class AppLocalizationsVi extends AppLocalizations {
@override
String get repeatYearly => 'Hằng năm';
+ @override
+ String get repeatEvery => 'Lặp lại mỗi';
+
+ @override
+ String get repeatOn => 'Repeat on';
+
+ @override
+ String get repeatEnd => 'Kết thúc lập lại';
+
+ @override
+ String get repeatNever => 'Never';
+
+ @override
+ String get repeatUntil => 'On date';
+
+ @override
+ String get repeatAfter => 'After a number of occurrences';
+
+ @override
+ String get repeatCount => 'Occurrences';
+
+ @override
+ String get repeatDayOfMonth => 'Days of month';
+
+ @override
+ String get repeatMonths => 'Months';
+
+ @override
+ String get repeatOrdinal => 'Weekday position';
+
+ @override
+ String get repeatSpecificDays => 'Specific days';
+
+ @override
+ String get repeatFirst => 'First';
+
+ @override
+ String get repeatSecond => 'Second';
+
+ @override
+ String get repeatThird => 'Third';
+
+ @override
+ String get repeatFourth => 'Fourth';
+
+ @override
+ String get repeatFifth => 'Fifth';
+
+ @override
+ String get repeatSecondToLast => 'Second to last';
+
+ @override
+ String get repeatLast => 'Last';
+
+ @override
+ String get repeatAnyDay => 'Day';
+
+ @override
+ String get repeatWeekday => 'Weekday';
+
+ @override
+ String get repeatWeekendDay => 'Weekend day';
+
+ @override
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
+
+ @override
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
+
+ @override
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
+
+ @override
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
+
+ @override
+ String repeatOnDaysSummary(String days) {
+ return 'on $days';
+ }
+
+ @override
+ String repeatOnMonthDaysSummary(String days) {
+ return 'on day $days';
+ }
+
+ @override
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
+
+ @override
+ String repeatInMonthsSummary(String months) {
+ return 'in $months';
+ }
+
+ @override
+ String repeatTimesSummary(int count) {
+ return '$count lần';
+ }
+
+ @override
+ String repeatUntilSummary(String date) {
+ return 'cho đến $date';
+ }
+
+ @override
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
+
@override
String get importance => 'Mức độ quan trọng';
@@ -912,6 +1211,26 @@ class AppLocalizationsVi extends AppLocalizations {
@override
String get createSubtask => 'Tạo công việc con';
+ @override
+ String get subtasks => 'Công việc con';
+
+ @override
+ String get duplicateTask => 'Duplicate task';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => 'Hide subtasks';
+
+ @override
+ String get hideClosedSubtasks => 'Hide closed subtasks';
+
@override
String get moveToTop => 'Chuyển lên đầu';
@@ -923,7 +1242,7 @@ class AppLocalizationsVi extends AppLocalizations {
@override
String deleteTaskConfirmation(String title) {
- return 'Xóa “$title” khỏi Google Tasks?';
+ return 'Xóa “$title”?';
}
@override
@@ -1244,6 +1563,224 @@ class AppLocalizationsVi extends AppLocalizations {
@override
String get noLocationsFound => 'Không tìm thấy địa điểm';
+ @override
+ String get requiredField => 'This field is required.';
+
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
+
+ @override
+ String get appleICloudProvider => 'Apple iCloud Calendar';
+
+ @override
+ String get nextcloudProvider => 'Nextcloud';
+
+ @override
+ String get appleICloudTasksProvider => 'Apple iCloud';
+
+ @override
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
+
+ @override
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
+
+ @override
+ String get addNextcloudAccount => 'Add Nextcloud account';
+
+ @override
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
+
+ @override
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
+
+ @override
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
+
+ @override
+ String get appleAccountEmail => 'Apple Account email';
+
+ @override
+ String get appleAppSpecificPassword => 'App-specific password';
+
+ @override
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
+
+ @override
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
+
+ @override
+ String get connectNextcloudTitle => 'Connect Nextcloud';
+
+ @override
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
+
+ @override
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
+
+ @override
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
+
+ @override
+ String get connectAccountAction => 'Connect';
+
+ @override
+ String get cancelAccountConnection => 'Cancel connection';
+
+ @override
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
+
+ @override
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
+
+ @override
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
+
+ @override
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
+
+ @override
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
+
+ @override
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
+
+ @override
+ String get collectionSettings => 'Calendars and task lists';
+
+ @override
+ String get calendarContent => 'Calendar events';
+
+ @override
+ String get taskContent => 'Tasks';
+
+ @override
+ String get readOnlySharedCollection => 'Read-only';
+
+ @override
+ String get pendingLocally => 'Pending locally';
+
+ @override
+ String get conflictBlocked => 'Blocked by conflict';
+
+ @override
+ String get authenticationBlocked => 'Blocked until reconnect';
+
+ @override
+ String get operationFailed => 'Operation failed';
+
+ @override
+ String get keepServerVersion => 'Keep server version';
+
+ @override
+ String get reapplyLocalChange => 'Review and reapply local change';
+
+ @override
+ String get duplicateLocalItem => 'Duplicate as new item';
+
+ @override
+ String get davConnectionState => 'Connection state';
+
+ @override
+ String get davConnected => 'Connected';
+
+ @override
+ String get davConnecting => 'Connecting…';
+
+ @override
+ String get davSignedOut => 'Signed out';
+
+ @override
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
+
+ @override
+ String get thisAndFutureUnavailable => 'This and future (not available)';
+
+ @override
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
+
+ @override
+ String get taskDueBeforeStart =>
+ 'Hạn chót không được trước thời gian bắt đầu.';
+
+ @override
+ String get taskStartDueTimeModeMismatch =>
+ 'Đặt giờ cho cả thời gian bắt đầu và hạn chót, hoặc đặt công việc là cả ngày.';
+
@override
String deleteCalendarConfirmation(String title) {
return 'Xóa “$title”?';
diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart
index 5d7f2bf..d23be00 100644
--- a/lib/l10n/generated/app_localizations_zh.dart
+++ b/lib/l10n/generated/app_localizations_zh.dart
@@ -16,7 +16,8 @@ class AppLocalizationsZh extends AppLocalizations {
String get appTitle => 'BusyMax';
@override
- String get connectGoogleAccount => '连接 Google 和 Microsoft 帐户以同步日历和任务。';
+ String get connectGoogleAccount =>
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
@override
String get googlePermissionsConsentNotice => '在 Google 权限页面上,同时选择日历和任务权限。';
@@ -39,7 +40,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get onboardingAccountsStepDescription =>
- '添加您要使用的所有 Google 和 Microsoft 帐户。BusyMax 会同步每个帐户中的日历、日程、任务列表和任务。';
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
@override
String get onboardingPreferencesStepTitle => '选择系统设置';
@@ -592,7 +593,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get removeAccountConfirmation =>
- '这会从此设备删除缓存的任务、日历、日程、提醒和待处理的离线更改。未同步的更改将丢失。不会从 Google 或 Microsoft 删除任何内容。';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess => '同时撤销 BusyMax 对此 Google 帐户的访问权限';
@@ -611,7 +612,22 @@ class AppLocalizationsZh extends AppLocalizations {
'该帐户已从此设备移除,但无法撤销 BusyMax 对您的 Google 帐户的访问权限。您可以在 Google 帐户中手动撤销该权限。';
@override
- String get newList => '新建列表';
+ String get newTaskList => '新建任务列表';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
@override
String get signInToViewTaskLists => '登录以查看任务列表。';
@@ -634,6 +650,17 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get deleteList => '删除列表';
+ @override
+ String get unshare => '取消共享';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
@override
String get builtInMicrosoftList => '内置';
@@ -646,6 +673,16 @@ class AppLocalizationsZh extends AppLocalizations {
return '从 Google Tasks 中删除“$title”?';
}
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
@override
String get deleteEvent => '删除日程';
@@ -767,6 +804,74 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get doneStatus => '已完成';
+ @override
+ String get taskStatus => 'Status';
+
+ @override
+ String get taskStatusNone => 'No status';
+
+ @override
+ String get taskStatusNeedsAction => '需要操作';
+
+ @override
+ String get taskStatusInProcess => '处理中';
+
+ @override
+ String get taskStatusCompleted => '完成';
+
+ @override
+ String get taskStatusCancelled => 'Cancelled';
+
+ @override
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
+
+ @override
+ String get completionDate => 'Completion date';
+
+ @override
+ String get priority => '优先级';
+
+ @override
+ String get priorityNone => 'No priority';
+
+ @override
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
+
+ @override
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
+
+ @override
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
+
+ @override
+ String get taskUrl => 'URL';
+
+ @override
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
+
+ @override
+ String get classification => 'Classification';
+
+ @override
+ String get classificationPublic => 'When shared, show the full task';
+
+ @override
+ String get classificationConfidential => 'When shared, show only busy';
+
+ @override
+ String get classificationPrivate => 'When shared, hide this task';
+
+ @override
+ String get pinTask => 'Pin task';
+
@override
String get notes => '备注';
@@ -803,6 +908,84 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get addReminder => '添加提醒';
+ @override
+ String get reminders => 'Reminders';
+
+ @override
+ String get noReminders => '无提醒';
+
+ @override
+ String get editReminder => 'Edit reminder';
+
+ @override
+ String get beforeTaskStarts => '任务开始前';
+
+ @override
+ String get beforeTaskDue => '任务截止前';
+
+ @override
+ String get afterTaskStarts => 'After the task starts';
+
+ @override
+ String get afterTaskDue => 'After the task is due';
+
+ @override
+ String get relativeToTaskStart => 'Relative to the task start date';
+
+ @override
+ String get relativeToTaskDue => 'Relative to the task due date';
+
+ @override
+ String get reminderTimeOfDay => 'Time of day';
+
+ @override
+ String get absoluteReminder => 'At a date and time';
+
+ @override
+ String get reminderAmount => 'Amount';
+
+ @override
+ String get reminderUnit => 'Unit';
+
+ @override
+ String get reminderUnitSeconds => 'Seconds';
+
+ @override
+ String get reminderUnitMinutes => 'Minutes';
+
+ @override
+ String get reminderUnitHours => 'Hours';
+
+ @override
+ String get reminderUnitDays => 'Days';
+
+ @override
+ String get reminderUnitWeeks => 'Weeks';
+
+ @override
+ String get reminderAtTaskStart => 'At the task start';
+
+ @override
+ String get reminderAtTaskDue => 'At the task due time';
+
+ @override
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
+
+ @override
+ String get relatedRemindersTitle => 'Keep related reminders?';
+
+ @override
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
+
+ @override
+ String get discardRelatedReminders => '舍弃提醒';
+
+ @override
+ String get keepRelatedReminders => '保留提醒';
+
@override
String get addGuest => '添加参与者';
@@ -836,6 +1019,123 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get repeatYearly => '每年';
+ @override
+ String get repeatEvery => '重复每';
+
+ @override
+ String get repeatOn => 'Repeat on';
+
+ @override
+ String get repeatEnd => '结束重复';
+
+ @override
+ String get repeatNever => 'Never';
+
+ @override
+ String get repeatUntil => 'On date';
+
+ @override
+ String get repeatAfter => 'After a number of occurrences';
+
+ @override
+ String get repeatCount => 'Occurrences';
+
+ @override
+ String get repeatDayOfMonth => 'Days of month';
+
+ @override
+ String get repeatMonths => 'Months';
+
+ @override
+ String get repeatOrdinal => 'Weekday position';
+
+ @override
+ String get repeatSpecificDays => 'Specific days';
+
+ @override
+ String get repeatFirst => 'First';
+
+ @override
+ String get repeatSecond => 'Second';
+
+ @override
+ String get repeatThird => 'Third';
+
+ @override
+ String get repeatFourth => 'Fourth';
+
+ @override
+ String get repeatFifth => 'Fifth';
+
+ @override
+ String get repeatSecondToLast => 'Second to last';
+
+ @override
+ String get repeatLast => 'Last';
+
+ @override
+ String get repeatAnyDay => 'Day';
+
+ @override
+ String get repeatWeekday => 'Weekday';
+
+ @override
+ String get repeatWeekendDay => 'Weekend day';
+
+ @override
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
+
+ @override
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
+
+ @override
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
+
+ @override
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
+
+ @override
+ String repeatOnDaysSummary(String days) {
+ return '在 $days';
+ }
+
+ @override
+ String repeatOnMonthDaysSummary(String days) {
+ return '在第 $days 天';
+ }
+
+ @override
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
+
+ @override
+ String repeatInMonthsSummary(String months) {
+ return '在 $months';
+ }
+
+ @override
+ String repeatTimesSummary(int count) {
+ return '$count次';
+ }
+
+ @override
+ String repeatUntilSummary(String date) {
+ return '至 $date';
+ }
+
+ @override
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
+
@override
String get importance => '重要性';
@@ -884,6 +1184,26 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get createSubtask => '创建子任务';
+ @override
+ String get subtasks => '子任务';
+
+ @override
+ String get duplicateTask => '复制任务';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => '隐藏子任务';
+
+ @override
+ String get hideClosedSubtasks => '隐藏关闭的子任务';
+
@override
String get moveToTop => '移到顶部';
@@ -895,7 +1215,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String deleteTaskConfirmation(String title) {
- return '从 Google Tasks 中删除“$title”?';
+ return '删除“$title”?';
}
@override
@@ -1211,141 +1531,358 @@ class AppLocalizationsZh extends AppLocalizations {
String get noLocationsFound => '未找到地点';
@override
- String deleteCalendarConfirmation(String title) {
- return '删除“$title”?';
- }
-}
+ String get requiredField => 'This field is required.';
-/// The translations for Chinese, using the Han script (`zh_Hans`).
-class AppLocalizationsZhHans extends AppLocalizationsZh {
- AppLocalizationsZhHans() : super('zh_Hans');
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
@override
- String get appTitle => 'BusyMax';
+ String get appleICloudProvider => 'Apple iCloud Calendar';
@override
- String get connectGoogleAccount => '连接 Google 和 Microsoft 帐户以同步日历和任务。';
+ String get nextcloudProvider => 'Nextcloud';
@override
- String get googlePermissionsConsentNotice => '在 Google 权限页面上,同时选择日历和任务权限。';
+ String get appleICloudTasksProvider => 'Apple iCloud';
@override
- String get googlePermissionsRequiredRetry =>
- '必须授予 Google 日历和 Google Tasks 权限。请重试并选中两个复选框。';
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
@override
- String get finishSetup => '完成设置';
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
@override
- String get continueSetup => '继续';
+ String get addNextcloudAccount => 'Add Nextcloud account';
@override
- String get onboardingSetupTitle => '设置 BusyMax';
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
@override
- String get onboardingAccountsStepTitle => '连接帐户';
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
@override
- String get onboardingAccountsStepDescription =>
- '添加您要使用的所有 Google 和 Microsoft 帐户。BusyMax 会同步每个帐户中的日历、日程、任务列表和任务。';
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
@override
- String get onboardingPreferencesStepTitle => '选择系统设置';
+ String get appleAccountEmail => 'Apple Account email';
@override
- String get onboardingPreferencesStepDescription =>
- '打开日程前,请设置桌面行为、提醒、通知详细程度和外观。';
+ String get appleAppSpecificPassword => 'App-specific password';
@override
- String get signInWithGoogle => '使用 Google 登录';
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
@override
- String get signInWithMicrosoft => '使用 Microsoft 登录';
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
@override
- String get googleTasksProvider => 'Google Tasks';
+ String get connectNextcloudTitle => 'Connect Nextcloud';
@override
- String get microsoftTodoProvider => 'Microsoft To Do';
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
@override
- String get providerNotConfigured => '尚未配置此服务。';
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
@override
- String get waitingForGoogleSignIn => '正在等待 Google 登录...';
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
@override
- String get waitingForMicrosoftSignIn => '正在等待 Microsoft 登录...';
+ String get connectAccountAction => 'Connect';
@override
- String get microsoftSignInNotConfigured =>
- '尚未配置 Microsoft 登录。请设置 MICROSOFT_OAUTH_CLIENT_ID。';
+ String get cancelAccountConnection => 'Cancel connection';
@override
- String get cancel => '取消';
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
@override
- String get close => '关闭';
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
@override
- String get exit => '退出';
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
@override
- String get options => '选项';
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
@override
- String get hide => '隐藏';
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
@override
- String get show => '显示';
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
@override
- String get export => '导出';
+ String get collectionSettings => 'Calendars and task lists';
@override
- String get save => '保存';
+ String get calendarContent => 'Calendar events';
@override
- String get settings => '设置';
+ String get taskContent => 'Tasks';
@override
- String get all => '全部';
+ String get readOnlySharedCollection => 'Read-only';
@override
- String get calendarEvents => '日程';
+ String get pendingLocally => 'Pending locally';
@override
- String get calendarTasks => '任务';
+ String get conflictBlocked => 'Blocked by conflict';
@override
- String get calendar => '日历';
+ String get authenticationBlocked => 'Blocked until reconnect';
@override
- String get calendars => '日历';
+ String get operationFailed => 'Operation failed';
@override
- String get newEvent => '新建日程';
+ String get keepServerVersion => 'Keep server version';
@override
- String get refreshCalendar => '刷新日历';
+ String get reapplyLocalChange => 'Review and reapply local change';
@override
- String get openInProvider => '在服务中打开';
+ String get duplicateLocalItem => 'Duplicate as new item';
@override
- String get hideFromSchedule => '从日程中隐藏';
+ String get davConnectionState => 'Connection state';
@override
- String get showInSchedule => '在日程中显示';
+ String get davConnected => 'Connected';
@override
- String get noCalendarsSynced => '尚未同步任何日历。';
+ String get davConnecting => 'Connecting…';
@override
- String get allDay => '全天';
+ String get davSignedOut => 'Signed out';
@override
- String moreItems(int count) {
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
+
+ @override
+ String get thisAndFutureUnavailable => 'This and future (not available)';
+
+ @override
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
+
+ @override
+ String get taskDueBeforeStart => '截止时间不能早于开始时间。';
+
+ @override
+ String get taskStartDueTimeModeMismatch => '请同时设置开始和截止时间,或将任务设为全天。';
+
+ @override
+ String deleteCalendarConfirmation(String title) {
+ return '删除“$title”?';
+ }
+}
+
+/// The translations for Chinese, using the Han script (`zh_Hans`).
+class AppLocalizationsZhHans extends AppLocalizationsZh {
+ AppLocalizationsZhHans() : super('zh_Hans');
+
+ @override
+ String get appTitle => 'BusyMax';
+
+ @override
+ String get connectGoogleAccount =>
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
+
+ @override
+ String get googlePermissionsConsentNotice => '在 Google 权限页面上,同时选择日历和任务权限。';
+
+ @override
+ String get googlePermissionsRequiredRetry =>
+ '必须授予 Google 日历和 Google Tasks 权限。请重试并选中两个复选框。';
+
+ @override
+ String get finishSetup => '完成设置';
+
+ @override
+ String get continueSetup => '继续';
+
+ @override
+ String get onboardingSetupTitle => '设置 BusyMax';
+
+ @override
+ String get onboardingAccountsStepTitle => '连接帐户';
+
+ @override
+ String get onboardingAccountsStepDescription =>
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
+
+ @override
+ String get onboardingPreferencesStepTitle => '选择系统设置';
+
+ @override
+ String get onboardingPreferencesStepDescription =>
+ '打开日程前,请设置桌面行为、提醒、通知详细程度和外观。';
+
+ @override
+ String get signInWithGoogle => '使用 Google 登录';
+
+ @override
+ String get signInWithMicrosoft => '使用 Microsoft 登录';
+
+ @override
+ String get googleTasksProvider => 'Google Tasks';
+
+ @override
+ String get microsoftTodoProvider => 'Microsoft To Do';
+
+ @override
+ String get providerNotConfigured => '尚未配置此服务。';
+
+ @override
+ String get waitingForGoogleSignIn => '正在等待 Google 登录...';
+
+ @override
+ String get waitingForMicrosoftSignIn => '正在等待 Microsoft 登录...';
+
+ @override
+ String get microsoftSignInNotConfigured =>
+ '尚未配置 Microsoft 登录。请设置 MICROSOFT_OAUTH_CLIENT_ID。';
+
+ @override
+ String get cancel => '取消';
+
+ @override
+ String get close => '关闭';
+
+ @override
+ String get exit => '退出';
+
+ @override
+ String get options => '选项';
+
+ @override
+ String get hide => '隐藏';
+
+ @override
+ String get show => '显示';
+
+ @override
+ String get export => '导出';
+
+ @override
+ String get save => '保存';
+
+ @override
+ String get settings => '设置';
+
+ @override
+ String get all => '全部';
+
+ @override
+ String get calendarEvents => '日程';
+
+ @override
+ String get calendarTasks => '任务';
+
+ @override
+ String get calendar => '日历';
+
+ @override
+ String get calendars => '日历';
+
+ @override
+ String get newEvent => '新建日程';
+
+ @override
+ String get refreshCalendar => '刷新日历';
+
+ @override
+ String get openInProvider => '在服务中打开';
+
+ @override
+ String get hideFromSchedule => '从日程中隐藏';
+
+ @override
+ String get showInSchedule => '在日程中显示';
+
+ @override
+ String get noCalendarsSynced => '尚未同步任何日历。';
+
+ @override
+ String get allDay => '全天';
+
+ @override
+ String moreItems(int count) {
return '还有 $count 项';
}
@@ -1800,7 +2337,7 @@ class AppLocalizationsZhHans extends AppLocalizationsZh {
@override
String get removeAccountConfirmation =>
- '这会从此设备删除缓存的任务、日历、日程、提醒和待处理的离线更改。未同步的更改将丢失。不会从 Google 或 Microsoft 删除任何内容。';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess => '同时撤销 BusyMax 对此 Google 帐户的访问权限';
@@ -1819,7 +2356,22 @@ class AppLocalizationsZhHans extends AppLocalizationsZh {
'该帐户已从此设备移除,但无法撤销 BusyMax 对您的 Google 帐户的访问权限。您可以在 Google 帐户中手动撤销该权限。';
@override
- String get newList => '新建列表';
+ String get newTaskList => '新建任务列表';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
@override
String get signInToViewTaskLists => '登录以查看任务列表。';
@@ -1842,6 +2394,17 @@ class AppLocalizationsZhHans extends AppLocalizationsZh {
@override
String get deleteList => '删除列表';
+ @override
+ String get unshare => '取消共享';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
@override
String get builtInMicrosoftList => '内置';
@@ -1854,6 +2417,16 @@ class AppLocalizationsZhHans extends AppLocalizationsZh {
return '从 Google Tasks 中删除“$title”?';
}
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
@override
String get deleteEvent => '删除日程';
@@ -1976,112 +2549,375 @@ class AppLocalizationsZhHans extends AppLocalizationsZh {
String get doneStatus => '已完成';
@override
- String get notes => '备注';
+ String get taskStatus => 'Status';
@override
- String get dueDate => '截止日期';
+ String get taskStatusNone => 'No status';
@override
- String get clearDueDate => '清除截止日期';
+ String get taskStatusNeedsAction => '需要操作';
@override
- String get dueTime => '截止时间';
+ String get taskStatusInProcess => '处理中';
@override
- String get startDate => '开始日期';
+ String get taskStatusCompleted => '完成';
@override
- String get startTime => '开始时间';
+ String get taskStatusCancelled => 'Cancelled';
@override
- String get endDate => '结束日期';
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
@override
- String get endTime => '结束时间';
+ String get completionDate => 'Completion date';
@override
- String get reminderDate => '提醒日期';
+ String get priority => '优先级';
@override
- String get reminderTime => '提醒时间';
+ String get priorityNone => 'No priority';
@override
- String get reminder => '提醒';
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
@override
- String get addReminder => '添加提醒';
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
@override
- String get addGuest => '添加参与者';
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
@override
- String get addGuestEmail => '添加参与者电子邮件';
+ String get taskUrl => 'URL';
@override
- String get removeReminder => '移除提醒';
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
@override
- String get off => '关闭';
+ String get classification => 'Classification';
@override
- String get repeat => '重复';
+ String get classificationPublic => 'When shared, show the full task';
@override
- String get repeatNone => '不重复';
+ String get classificationConfidential => 'When shared, show only busy';
@override
- String get noneValue => '无';
+ String get classificationPrivate => 'When shared, hide this task';
@override
- String get repeatDaily => '每天';
+ String get pinTask => 'Pin task';
@override
- String get repeatWeekly => '每周';
+ String get notes => '备注';
@override
- String get repeatMonthly => '每月';
+ String get dueDate => '截止日期';
@override
- String get repeatYearly => '每年';
+ String get clearDueDate => '清除截止日期';
@override
- String get importance => '重要性';
+ String get dueTime => '截止时间';
@override
- String get importanceLow => '低';
+ String get startDate => '开始日期';
@override
- String get importanceNormal => '普通';
+ String get startTime => '开始时间';
@override
- String get importanceHigh => '高';
+ String get endDate => '结束日期';
@override
- String get categories => '类别';
+ String get endTime => '结束时间';
@override
- String get scheduleSection => '日程';
+ String get reminderDate => '提醒日期';
@override
- String get dueGroup => '截止';
+ String get reminderTime => '提醒时间';
@override
- String get startGroup => '开始';
+ String get reminder => '提醒';
@override
- String get reminderGroup => '提醒';
+ String get addReminder => '添加提醒';
@override
- String get organizationSection => '整理';
+ String get reminders => 'Reminders';
@override
- String get actionsSection => '操作';
+ String get noReminders => '无提醒';
@override
- String get advancedSection => '高级';
+ String get editReminder => 'Edit reminder';
@override
- String get addCategory => '添加类别';
+ String get beforeTaskStarts => '任务开始前';
+
+ @override
+ String get beforeTaskDue => '任务截止前';
+
+ @override
+ String get afterTaskStarts => 'After the task starts';
+
+ @override
+ String get afterTaskDue => 'After the task is due';
+
+ @override
+ String get relativeToTaskStart => 'Relative to the task start date';
+
+ @override
+ String get relativeToTaskDue => 'Relative to the task due date';
+
+ @override
+ String get reminderTimeOfDay => 'Time of day';
+
+ @override
+ String get absoluteReminder => 'At a date and time';
+
+ @override
+ String get reminderAmount => 'Amount';
+
+ @override
+ String get reminderUnit => 'Unit';
+
+ @override
+ String get reminderUnitSeconds => 'Seconds';
+
+ @override
+ String get reminderUnitMinutes => 'Minutes';
+
+ @override
+ String get reminderUnitHours => 'Hours';
+
+ @override
+ String get reminderUnitDays => 'Days';
+
+ @override
+ String get reminderUnitWeeks => 'Weeks';
+
+ @override
+ String get reminderAtTaskStart => 'At the task start';
+
+ @override
+ String get reminderAtTaskDue => 'At the task due time';
+
+ @override
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
+
+ @override
+ String get relatedRemindersTitle => 'Keep related reminders?';
+
+ @override
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
+
+ @override
+ String get discardRelatedReminders => '舍弃提醒';
+
+ @override
+ String get keepRelatedReminders => '保留提醒';
+
+ @override
+ String get addGuest => '添加参与者';
+
+ @override
+ String get addGuestEmail => '添加参与者电子邮件';
+
+ @override
+ String get removeReminder => '移除提醒';
+
+ @override
+ String get off => '关闭';
+
+ @override
+ String get repeat => '重复';
+
+ @override
+ String get repeatNone => '不重复';
+
+ @override
+ String get noneValue => '无';
+
+ @override
+ String get repeatDaily => '每天';
+
+ @override
+ String get repeatWeekly => '每周';
+
+ @override
+ String get repeatMonthly => '每月';
+
+ @override
+ String get repeatYearly => '每年';
+
+ @override
+ String get repeatEvery => '重复每';
+
+ @override
+ String get repeatOn => 'Repeat on';
+
+ @override
+ String get repeatEnd => '结束重复';
+
+ @override
+ String get repeatNever => 'Never';
+
+ @override
+ String get repeatUntil => 'On date';
+
+ @override
+ String get repeatAfter => 'After a number of occurrences';
+
+ @override
+ String get repeatCount => 'Occurrences';
+
+ @override
+ String get repeatDayOfMonth => 'Days of month';
+
+ @override
+ String get repeatMonths => 'Months';
+
+ @override
+ String get repeatOrdinal => 'Weekday position';
+
+ @override
+ String get repeatSpecificDays => 'Specific days';
+
+ @override
+ String get repeatFirst => 'First';
+
+ @override
+ String get repeatSecond => 'Second';
+
+ @override
+ String get repeatThird => 'Third';
+
+ @override
+ String get repeatFourth => 'Fourth';
+
+ @override
+ String get repeatFifth => 'Fifth';
+
+ @override
+ String get repeatSecondToLast => 'Second to last';
+
+ @override
+ String get repeatLast => 'Last';
+
+ @override
+ String get repeatAnyDay => 'Day';
+
+ @override
+ String get repeatWeekday => 'Weekday';
+
+ @override
+ String get repeatWeekendDay => 'Weekend day';
+
+ @override
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
+
+ @override
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
+
+ @override
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
+
+ @override
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
+
+ @override
+ String repeatOnDaysSummary(String days) {
+ return '在 $days';
+ }
+
+ @override
+ String repeatOnMonthDaysSummary(String days) {
+ return '在第 $days 天';
+ }
+
+ @override
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
+
+ @override
+ String repeatInMonthsSummary(String months) {
+ return '在 $months';
+ }
+
+ @override
+ String repeatTimesSummary(int count) {
+ return '$count次';
+ }
+
+ @override
+ String repeatUntilSummary(String date) {
+ return '至 $date';
+ }
+
+ @override
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
+
+ @override
+ String get importance => '重要性';
+
+ @override
+ String get importanceLow => '低';
+
+ @override
+ String get importanceNormal => '普通';
+
+ @override
+ String get importanceHigh => '高';
+
+ @override
+ String get categories => '类别';
+
+ @override
+ String get scheduleSection => '日程';
+
+ @override
+ String get dueGroup => '截止';
+
+ @override
+ String get startGroup => '开始';
+
+ @override
+ String get reminderGroup => '提醒';
+
+ @override
+ String get organizationSection => '整理';
+
+ @override
+ String get actionsSection => '操作';
+
+ @override
+ String get advancedSection => '高级';
+
+ @override
+ String get addCategory => '添加类别';
@override
String get list => '列表';
@@ -2092,6 +2928,26 @@ class AppLocalizationsZhHans extends AppLocalizationsZh {
@override
String get createSubtask => '创建子任务';
+ @override
+ String get subtasks => '子任务';
+
+ @override
+ String get duplicateTask => '复制任务';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => '隐藏子任务';
+
+ @override
+ String get hideClosedSubtasks => '隐藏关闭的子任务';
+
@override
String get moveToTop => '移到顶部';
@@ -2103,7 +2959,7 @@ class AppLocalizationsZhHans extends AppLocalizationsZh {
@override
String deleteTaskConfirmation(String title) {
- return '从 Google Tasks 中删除“$title”?';
+ return '删除“$title”?';
}
@override
@@ -2407,16 +3263,232 @@ class AppLocalizationsZhHans extends AppLocalizationsZh {
}
@override
- String get readOnlyCalendar => '此日历为只读。';
+ String get readOnlyCalendar => '此日历为只读。';
+
+ @override
+ String get selectTimeZone => '选择时区';
+
+ @override
+ String get searchLocations => '搜索地点';
+
+ @override
+ String get noLocationsFound => '未找到地点';
+
+ @override
+ String get requiredField => 'This field is required.';
+
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
+
+ @override
+ String get appleICloudProvider => 'Apple iCloud Calendar';
+
+ @override
+ String get nextcloudProvider => 'Nextcloud';
+
+ @override
+ String get appleICloudTasksProvider => 'Apple iCloud';
+
+ @override
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
+
+ @override
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
+
+ @override
+ String get addNextcloudAccount => 'Add Nextcloud account';
+
+ @override
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
+
+ @override
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
+
+ @override
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
+
+ @override
+ String get appleAccountEmail => 'Apple Account email';
+
+ @override
+ String get appleAppSpecificPassword => 'App-specific password';
+
+ @override
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
+
+ @override
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
+
+ @override
+ String get connectNextcloudTitle => 'Connect Nextcloud';
+
+ @override
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
+
+ @override
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
+
+ @override
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
+
+ @override
+ String get connectAccountAction => 'Connect';
+
+ @override
+ String get cancelAccountConnection => 'Cancel connection';
+
+ @override
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
+
+ @override
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
+
+ @override
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
+
+ @override
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
+
+ @override
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
+
+ @override
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
+
+ @override
+ String get collectionSettings => 'Calendars and task lists';
+
+ @override
+ String get calendarContent => 'Calendar events';
+
+ @override
+ String get taskContent => 'Tasks';
+
+ @override
+ String get readOnlySharedCollection => 'Read-only';
+
+ @override
+ String get pendingLocally => 'Pending locally';
+
+ @override
+ String get conflictBlocked => 'Blocked by conflict';
+
+ @override
+ String get authenticationBlocked => 'Blocked until reconnect';
+
+ @override
+ String get operationFailed => 'Operation failed';
+
+ @override
+ String get keepServerVersion => 'Keep server version';
+
+ @override
+ String get reapplyLocalChange => 'Review and reapply local change';
+
+ @override
+ String get duplicateLocalItem => 'Duplicate as new item';
+
+ @override
+ String get davConnectionState => 'Connection state';
+
+ @override
+ String get davConnected => 'Connected';
+
+ @override
+ String get davConnecting => 'Connecting…';
+
+ @override
+ String get davSignedOut => 'Signed out';
+
+ @override
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
@override
- String get selectTimeZone => '选择时区';
+ String get thisAndFutureUnavailable => 'This and future (not available)';
@override
- String get searchLocations => '搜索地点';
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
@override
- String get noLocationsFound => '未找到地点';
+ String get taskDueBeforeStart => '截止时间不能早于开始时间。';
+
+ @override
+ String get taskStartDueTimeModeMismatch => '请同时设置开始和截止时间,或将任务设为全天。';
@override
String deleteCalendarConfirmation(String title) {
@@ -2432,7 +3504,8 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
String get appTitle => 'BusyMax';
@override
- String get connectGoogleAccount => '連結 Google 和 Microsoft 帳戶以同步行事曆和待辦事項。';
+ String get connectGoogleAccount =>
+ 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.';
@override
String get googlePermissionsConsentNotice => '在 Google 權限畫面中,同時選取行事曆和待辦事項權限。';
@@ -2455,7 +3528,7 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
@override
String get onboardingAccountsStepDescription =>
- '新增您要使用的所有 Google 和 Microsoft 帳戶。BusyMax 會同步每個帳戶中的行事曆、活動、待辦清單和待辦事項。';
+ 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.';
@override
String get onboardingPreferencesStepTitle => '選擇系統設定';
@@ -3008,7 +4081,7 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
@override
String get removeAccountConfirmation =>
- '這會從此裝置刪除快取的待辦事項、行事曆、活動、提醒和待處理的離線變更。未同步的變更將會遺失。不會從 Google 或 Microsoft 刪除任何內容。';
+ 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.';
@override
String get revokeGoogleAccess => '同時撤銷 BusyMax 對此 Google 帳戶的存取權';
@@ -3027,230 +4100,529 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
'該帳戶已從此裝置移除,但無法撤銷 BusyMax 對您的 Google 帳戶的存取權。您可以在 Google 帳戶中手動撤銷該權限。';
@override
- String get newList => '新增清單';
+ String get newTaskList => '新增待辦清單';
+
+ @override
+ String taskListCreateFailed(String error) {
+ return 'Could not create the task list: $error';
+ }
+
+ @override
+ String taskListRenameFailed(String error) {
+ return 'Could not rename the task list: $error';
+ }
+
+ @override
+ String taskListDeleteFailed(String error) {
+ return 'Could not delete the task list: $error';
+ }
+
+ @override
+ String get signInToViewTaskLists => '登入以查看待辦清單。';
+
+ @override
+ String get noTaskListsSynced => '尚未同步任何待辦清單。';
+
+ @override
+ String get listActions => '清單動作';
+
+ @override
+ String get rename => '重新命名';
+
+ @override
+ String get delete => '刪除';
+
+ @override
+ String get renameList => '重新命名清單';
+
+ @override
+ String get deleteList => '刪除清單';
+
+ @override
+ String get unshare => '撤回分享';
+
+ @override
+ String get readOnlyTaskListCannotRename =>
+ 'This task list is read-only and cannot be renamed.';
+
+ @override
+ String get taskListCannotDelete =>
+ 'This task list cannot be deleted with your current permissions.';
+
+ @override
+ String get builtInMicrosoftList => '內建';
+
+ @override
+ String get builtInMicrosoftListCannotRenameDelete =>
+ '無法重新命名或刪除 Microsoft To Do 內建清單。';
+
+ @override
+ String deleteListConfirmation(String title) {
+ return '要從 Google Tasks 刪除「$title」嗎?';
+ }
+
+ @override
+ String deleteTaskListConfirmation(String title) {
+ return 'Delete \"$title\" and all of its tasks?';
+ }
+
+ @override
+ String unshareTaskListConfirmation(String title) {
+ return 'Unshare \"$title\" from this account?';
+ }
+
+ @override
+ String get deleteEvent => '刪除活動';
+
+ @override
+ String get title => '標題';
+
+ @override
+ String get create => '新增';
+
+ @override
+ String get newTask => '新增待辦事項';
+
+ @override
+ String get clearCompleted => '清除已完成項目';
+
+ @override
+ String get refreshList => '重新整理清單';
+
+ @override
+ String get refreshAll => '全部重新整理';
+
+ @override
+ String get listRefreshed => '清單已重新整理。';
+
+ @override
+ String get allTasksRefreshed => '所有帳戶都已重新整理。';
+
+ @override
+ String exportedFile(String path) {
+ return '已匯出至 $path';
+ }
+
+ @override
+ String exportFailed(String error) {
+ return '匯出失敗:$error';
+ }
+
+ @override
+ String refreshFailed(String error) {
+ return '重新整理失敗:$error';
+ }
+
+ @override
+ String get selectOrCreateTaskList => '請選擇或建立待辦清單以開始使用。';
+
+ @override
+ String get signInToViewTasks => '登入以查看待辦事項。';
+
+ @override
+ String get noTasks => '沒有待辦事項。';
+
+ @override
+ String get noTasksYet => '還沒有待辦事項';
+
+ @override
+ String get noTasksYetMessage => '建立待辦事項或重新整理帳戶以開始使用。';
+
+ @override
+ String get noTasksInList => '此清單中沒有待辦事項。';
+
+ @override
+ String get overdue => '已逾期';
+
+ @override
+ String get today => '今天';
+
+ @override
+ String get tomorrow => '明天';
+
+ @override
+ String get upcoming => '即將到期';
+
+ @override
+ String get noDate => '無日期';
+
+ @override
+ String get completed => '已完成';
+
+ @override
+ String duePrefix(String date) {
+ return '$date 到期';
+ }
+
+ @override
+ String dateTimeDisplay(String date, String time) {
+ return '$date · $time';
+ }
+
+ @override
+ String get taskDetails => '待辦事項詳細資料';
+
+ @override
+ String get editTask => '編輯待辦事項';
+
+ @override
+ String get noTaskSelected => '未選取待辦事項。';
+
+ @override
+ String get noTaskSelectedHelper => '選擇待辦事項以查看和編輯詳細資料。';
+
+ @override
+ String get taskUnavailable => '無法使用待辦事項。';
+
+ @override
+ String get signInToEditTasks => '登入以編輯待辦事項。';
+
+ @override
+ String get refreshTask => '重新整理待辦事項';
+
+ @override
+ String get primarySection => '主要資訊';
+
+ @override
+ String get statusSection => '狀態';
+
+ @override
+ String get openStatus => '未完成';
+
+ @override
+ String get doneStatus => '已完成';
+
+ @override
+ String get taskStatus => 'Status';
+
+ @override
+ String get taskStatusNone => 'No status';
+
+ @override
+ String get taskStatusNeedsAction => '需要動作';
+
+ @override
+ String get taskStatusInProcess => '進行中';
+
+ @override
+ String get taskStatusCompleted => '完成';
+
+ @override
+ String get taskStatusCancelled => 'Cancelled';
+
+ @override
+ String completionPercent(int percent) {
+ return '$percent% completed';
+ }
+
+ @override
+ String get completionDate => 'Completion date';
+
+ @override
+ String get priority => '優先';
+
+ @override
+ String get priorityNone => 'No priority';
+
+ @override
+ String priorityHighValue(int priority) {
+ return 'Priority $priority · High';
+ }
+
+ @override
+ String priorityMediumValue(int priority) {
+ return 'Priority $priority · Medium';
+ }
+
+ @override
+ String priorityLowValue(int priority) {
+ return 'Priority $priority · Low';
+ }
+
+ @override
+ String get taskUrl => 'URL';
+
+ @override
+ String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.';
+
+ @override
+ String get classification => 'Classification';
+
+ @override
+ String get classificationPublic => 'When shared, show the full task';
+
+ @override
+ String get classificationConfidential => 'When shared, show only busy';
+
+ @override
+ String get classificationPrivate => 'When shared, hide this task';
+
+ @override
+ String get pinTask => 'Pin task';
+
+ @override
+ String get notes => '備註';
+
+ @override
+ String get dueDate => '到期日';
+
+ @override
+ String get clearDueDate => '清除到期日';
+
+ @override
+ String get dueTime => '到期時間';
+
+ @override
+ String get startDate => '開始日期';
+
+ @override
+ String get startTime => '開始時間';
+
+ @override
+ String get endDate => '結束日期';
+
+ @override
+ String get endTime => '結束時間';
+
+ @override
+ String get reminderDate => '提醒日期';
+
+ @override
+ String get reminderTime => '提醒時間';
@override
- String get signInToViewTaskLists => '登入以查看待辦清單。';
+ String get reminder => '提醒';
@override
- String get noTaskListsSynced => '尚未同步任何待辦清單。';
+ String get addReminder => '新增提醒';
@override
- String get listActions => '清單動作';
+ String get reminders => 'Reminders';
@override
- String get rename => '重新命名';
+ String get noReminders => '無提醒';
@override
- String get delete => '刪除';
+ String get editReminder => 'Edit reminder';
@override
- String get renameList => '重新命名清單';
+ String get beforeTaskStarts => '在任務開始前';
@override
- String get deleteList => '刪除清單';
+ String get beforeTaskDue => '任務截止前';
@override
- String get builtInMicrosoftList => '內建';
+ String get afterTaskStarts => 'After the task starts';
@override
- String get builtInMicrosoftListCannotRenameDelete =>
- '無法重新命名或刪除 Microsoft To Do 內建清單。';
+ String get afterTaskDue => 'After the task is due';
@override
- String deleteListConfirmation(String title) {
- return '要從 Google Tasks 刪除「$title」嗎?';
- }
+ String get relativeToTaskStart => 'Relative to the task start date';
@override
- String get deleteEvent => '刪除活動';
+ String get relativeToTaskDue => 'Relative to the task due date';
@override
- String get title => '標題';
+ String get reminderTimeOfDay => 'Time of day';
@override
- String get create => '新增';
+ String get absoluteReminder => 'At a date and time';
@override
- String get newTask => '新增待辦事項';
+ String get reminderAmount => 'Amount';
@override
- String get clearCompleted => '清除已完成項目';
+ String get reminderUnit => 'Unit';
@override
- String get refreshList => '重新整理清單';
+ String get reminderUnitSeconds => 'Seconds';
@override
- String get refreshAll => '全部重新整理';
+ String get reminderUnitMinutes => 'Minutes';
@override
- String get listRefreshed => '清單已重新整理。';
+ String get reminderUnitHours => 'Hours';
@override
- String get allTasksRefreshed => '所有帳戶都已重新整理。';
+ String get reminderUnitDays => 'Days';
@override
- String exportedFile(String path) {
- return '已匯出至 $path';
- }
+ String get reminderUnitWeeks => 'Weeks';
@override
- String exportFailed(String error) {
- return '匯出失敗:$error';
- }
+ String get reminderAtTaskStart => 'At the task start';
@override
- String refreshFailed(String error) {
- return '重新整理失敗:$error';
- }
+ String get reminderAtTaskDue => 'At the task due time';
@override
- String get selectOrCreateTaskList => '請選擇或建立待辦清單以開始使用。';
+ String get unsupportedReminder =>
+ 'This reminder type is preserved but its time cannot be edited.';
@override
- String get signInToViewTasks => '登入以查看待辦事項。';
+ String get relatedRemindersTitle => 'Keep related reminders?';
@override
- String get noTasks => '沒有待辦事項。';
+ String relatedRemindersDescription(int count) {
+ return 'This date has $count related reminders. Keep them at their current date and time?';
+ }
@override
- String get noTasksYet => '還沒有待辦事項';
+ String get discardRelatedReminders => '捨棄提醒';
@override
- String get noTasksYetMessage => '建立待辦事項或重新整理帳戶以開始使用。';
+ String get keepRelatedReminders => '保留提醒';
@override
- String get noTasksInList => '此清單中沒有待辦事項。';
+ String get addGuest => '新增參與者';
@override
- String get overdue => '已逾期';
+ String get addGuestEmail => '新增參與者電子郵件';
@override
- String get today => '今天';
+ String get removeReminder => '移除提醒';
@override
- String get tomorrow => '明天';
+ String get off => '關閉';
@override
- String get upcoming => '即將到期';
+ String get repeat => '重複';
@override
- String get noDate => '無日期';
+ String get repeatNone => '不重複';
@override
- String get completed => '已完成';
+ String get noneValue => '無';
@override
- String duePrefix(String date) {
- return '$date 到期';
- }
+ String get repeatDaily => '每天';
@override
- String dateTimeDisplay(String date, String time) {
- return '$date · $time';
- }
+ String get repeatWeekly => '每週';
@override
- String get taskDetails => '待辦事項詳細資料';
+ String get repeatMonthly => '每月';
@override
- String get editTask => '編輯待辦事項';
+ String get repeatYearly => '每年';
@override
- String get noTaskSelected => '未選取待辦事項。';
+ String get repeatEvery => '重複循環';
@override
- String get noTaskSelectedHelper => '選擇待辦事項以查看和編輯詳細資料。';
+ String get repeatOn => 'Repeat on';
@override
- String get taskUnavailable => '無法使用待辦事項。';
+ String get repeatEnd => '停止重複';
@override
- String get signInToEditTasks => '登入以編輯待辦事項。';
+ String get repeatNever => 'Never';
@override
- String get refreshTask => '重新整理待辦事項';
+ String get repeatUntil => 'On date';
@override
- String get primarySection => '主要資訊';
+ String get repeatAfter => 'After a number of occurrences';
@override
- String get statusSection => '狀態';
+ String get repeatCount => 'Occurrences';
@override
- String get openStatus => '未完成';
+ String get repeatDayOfMonth => 'Days of month';
@override
- String get doneStatus => '已完成';
+ String get repeatMonths => 'Months';
@override
- String get notes => '備註';
+ String get repeatOrdinal => 'Weekday position';
@override
- String get dueDate => '到期日';
+ String get repeatSpecificDays => 'Specific days';
@override
- String get clearDueDate => '清除到期日';
+ String get repeatFirst => 'First';
@override
- String get dueTime => '到期時間';
+ String get repeatSecond => 'Second';
@override
- String get startDate => '開始日期';
+ String get repeatThird => 'Third';
@override
- String get startTime => '開始時間';
+ String get repeatFourth => 'Fourth';
@override
- String get endDate => '結束日期';
+ String get repeatFifth => 'Fifth';
@override
- String get endTime => '結束時間';
+ String get repeatSecondToLast => 'Second to last';
@override
- String get reminderDate => '提醒日期';
+ String get repeatLast => 'Last';
@override
- String get reminderTime => '提醒時間';
+ String get repeatAnyDay => 'Day';
@override
- String get reminder => '提醒';
+ String get repeatWeekday => 'Weekday';
@override
- String get addReminder => '新增提醒';
+ String get repeatWeekendDay => 'Weekend day';
@override
- String get addGuest => '新增參與者';
+ String repeatEveryDays(int count) {
+ return 'Every $count days';
+ }
@override
- String get addGuestEmail => '新增參與者電子郵件';
+ String repeatEveryWeeks(int count) {
+ return 'Every $count weeks';
+ }
@override
- String get removeReminder => '移除提醒';
+ String repeatEveryMonths(int count) {
+ return 'Every $count months';
+ }
@override
- String get off => '關閉';
+ String repeatEveryYears(int count) {
+ return 'Every $count years';
+ }
@override
- String get repeat => '重複';
+ String repeatOnDaysSummary(String days) {
+ return '於 $days';
+ }
@override
- String get repeatNone => '不重複';
+ String repeatOnMonthDaysSummary(String days) {
+ return '於 $days 天';
+ }
@override
- String get noneValue => '無';
+ String repeatOnOrdinalSummary(String ordinal, String days) {
+ return 'on the $ordinal $days';
+ }
@override
- String get repeatDaily => '每天';
+ String repeatInMonthsSummary(String months) {
+ return '在 $months';
+ }
@override
- String get repeatWeekly => '每週';
+ String repeatTimesSummary(int count) {
+ return '$count 次';
+ }
@override
- String get repeatMonthly => '每月';
+ String repeatUntilSummary(String date) {
+ return '到 $date';
+ }
@override
- String get repeatYearly => '每年';
+ String get unsupportedRecurrencePreserved =>
+ 'This recurrence rule uses options that this editor does not change.';
@override
String get importance => '重要性';
@@ -3301,6 +4673,26 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
@override
String get createSubtask => '建立子待辦事項';
+ @override
+ String get subtasks => '子待辦事項';
+
+ @override
+ String get duplicateTask => '再製任務';
+
+ @override
+ String get taskDuplicated => 'Task duplicated.';
+
+ @override
+ String taskDuplicateFailed(String error) {
+ return 'Could not duplicate the task: $error';
+ }
+
+ @override
+ String get hideSubtasks => '隱藏子工作項目';
+
+ @override
+ String get hideClosedSubtasks => '隱藏已關閉的子工作項目';
+
@override
String get moveToTop => '移至頂端';
@@ -3312,7 +4704,7 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
@override
String deleteTaskConfirmation(String title) {
- return '要從 Google Tasks 刪除「$title」嗎?';
+ return '要刪除「$title」嗎?';
}
@override
@@ -3627,6 +5019,222 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
@override
String get noLocationsFound => '找不到地點';
+ @override
+ String get requiredField => 'This field is required.';
+
+ @override
+ String get providerConnectionDescription =>
+ 'Connect calendars and tasks from one of these providers.';
+
+ @override
+ String get appleICloudProvider => 'Apple iCloud Calendar';
+
+ @override
+ String get nextcloudProvider => 'Nextcloud';
+
+ @override
+ String get appleICloudTasksProvider => 'Apple iCloud';
+
+ @override
+ String get nextcloudTasksProvider => 'Nextcloud Tasks';
+
+ @override
+ String get addAppleICloudAccount => 'Add Apple iCloud Calendar account';
+
+ @override
+ String get addNextcloudAccount => 'Add Nextcloud account';
+
+ @override
+ String get waitingForAppleICloud => 'Connecting to Apple iCloud…';
+
+ @override
+ String get waitingForNextcloud => 'Waiting for Nextcloud authorization…';
+
+ @override
+ String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar';
+
+ @override
+ String get appleAccountEmail => 'Apple Account email';
+
+ @override
+ String get appleAppSpecificPassword => 'App-specific password';
+
+ @override
+ String get appleAppSpecificPasswordHelp =>
+ 'Create an app-specific password after enabling two-factor authentication for your Apple Account.';
+
+ @override
+ String get appleAppSpecificPasswordResetWarning =>
+ 'Resetting your Apple Account password revokes app-specific passwords.';
+
+ @override
+ String get connectNextcloudTitle => 'Connect Nextcloud';
+
+ @override
+ String get nextcloudServerUrl => 'Nextcloud server or CalDAV address';
+
+ @override
+ String get nextcloudServerUrlHelp =>
+ 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.';
+
+ @override
+ String get nextcloudBrowserAuthorizationHelp =>
+ 'BusyMax will open your browser. Approve access there, then return to BusyMax.';
+
+ @override
+ String get connectAccountAction => 'Connect';
+
+ @override
+ String get cancelAccountConnection => 'Cancel connection';
+
+ @override
+ String get nextcloudAccountRemovedRevokeFailed =>
+ 'The account was removed locally, but its Nextcloud app password could not be revoked.';
+
+ @override
+ String get davCachedOfflineNotice =>
+ 'Calendar and task data is cached locally for offline use.';
+
+ @override
+ String get davReauthenticationRequired =>
+ 'Reconnect this account to resume synchronization.';
+
+ @override
+ String get davTemporarilyUnavailable =>
+ 'This account is temporarily unavailable.';
+
+ @override
+ String get davPermissionChanged =>
+ 'Server permissions changed. Pending edits are paused.';
+
+ @override
+ String get davUnsupportedServer =>
+ 'This server or provider profile is not supported.';
+
+ @override
+ String get collectionSettings => 'Calendars and task lists';
+
+ @override
+ String get calendarContent => 'Calendar events';
+
+ @override
+ String get taskContent => 'Tasks';
+
+ @override
+ String get readOnlySharedCollection => 'Read-only';
+
+ @override
+ String get pendingLocally => 'Pending locally';
+
+ @override
+ String get conflictBlocked => 'Blocked by conflict';
+
+ @override
+ String get authenticationBlocked => 'Blocked until reconnect';
+
+ @override
+ String get operationFailed => 'Operation failed';
+
+ @override
+ String get keepServerVersion => 'Keep server version';
+
+ @override
+ String get reapplyLocalChange => 'Review and reapply local change';
+
+ @override
+ String get duplicateLocalItem => 'Duplicate as new item';
+
+ @override
+ String get davConnectionState => 'Connection state';
+
+ @override
+ String get davConnected => 'Connected';
+
+ @override
+ String get davConnecting => 'Connecting…';
+
+ @override
+ String get davSignedOut => 'Signed out';
+
+ @override
+ String davLastSuccessfulSync(String time) {
+ return 'Last successful sync: $time';
+ }
+
+ @override
+ String get davNeverSynced => 'Not synchronized yet';
+
+ @override
+ String get refreshCollections => 'Refresh calendars and task lists';
+
+ @override
+ String nextcloudServerHost(String host) {
+ return 'Server: $host';
+ }
+
+ @override
+ String get collectionSupportsEvents => 'Event calendar';
+
+ @override
+ String get collectionSupportsTasks => 'Task list';
+
+ @override
+ String get collectionSupportsEventsAndTasks => 'Events and tasks';
+
+ @override
+ String get writableCollection => 'Writable';
+
+ @override
+ String get sharedCollection => 'Shared';
+
+ @override
+ String collectionLastSynced(String time) {
+ return 'Last synchronized: $time';
+ }
+
+ @override
+ String collectionSyncError(String code) {
+ return 'Sync issue: $code';
+ }
+
+ @override
+ String get syncConflicts => 'Synchronization conflicts';
+
+ @override
+ String remoteChangedAt(String time) {
+ return 'Server changed: $time';
+ }
+
+ @override
+ String localPendingEdit(String summary) {
+ return 'Local edit: $summary';
+ }
+
+ @override
+ String get conflictResolutionFailed => 'The conflict could not be resolved.';
+
+ @override
+ String get recurringEventScope => 'Recurring event scope';
+
+ @override
+ String get entireSeries => 'Entire series';
+
+ @override
+ String get singleOccurrence => 'This occurrence';
+
+ @override
+ String get thisAndFutureUnavailable => 'This and future (not available)';
+
+ @override
+ String get chooseRecurringEventScope =>
+ 'Choose whether this change applies to the entire series or only this occurrence.';
+
+ @override
+ String get taskDueBeforeStart => '到期時間不得早於開始時間。';
+
+ @override
+ String get taskStartDueTimeModeMismatch => '請同時設定開始與到期時間,或將工作設為全天。';
+
@override
String deleteCalendarConfirmation(String title) {
return '要刪除「$title」嗎?';
diff --git a/lib/src/app/app_bootstrap.dart b/lib/src/app/app_bootstrap.dart
index 29d2dc4..059af6a 100644
--- a/lib/src/app/app_bootstrap.dart
+++ b/lib/src/app/app_bootstrap.dart
@@ -5,10 +5,22 @@ import 'package:drift/drift.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
+import 'package:uuid/uuid.dart';
import '../config/build_config.dart';
import '../core/time/local_time_zone.dart';
import '../db/app_database.dart';
+import '../dav/auth/dav_account_onboarding_service.dart';
+import '../dav/auth/nextcloud_app_password_revoker.dart';
+import '../dav/auth/nextcloud_login_flow_v2.dart';
+import '../dav/dav_provider_profile.dart';
+import '../dav/discovery/dav_discovery_service.dart';
+import '../dav/http/dav_http_transport.dart';
+import '../dav/mutation/dav_conflict_repository.dart';
+import '../dav/mutation/dav_pending_operations.dart';
+import '../dav/mutation/dav_task_list_mutation_service.dart';
+import '../dav/sync/dav_account_sync_engine.dart';
+import '../dav/storage/dav_settings_repository.dart';
import '../features/calendar/data/calendar_repository.dart';
import '../features/accounts/data/accounts_repository.dart';
import '../features/auth/data/auth_repository.dart';
@@ -24,20 +36,22 @@ import '../features/sync/sync_auth_error.dart';
import '../features/sync/sync_engine.dart';
import '../features/task_lists/data/task_lists_repository.dart';
import '../features/tasks/data/tasks_repository.dart';
+import '../features/tasks/domain/task_remote_client.dart';
import '../google_tasks/api/google_tasks_api_client.dart';
import '../google_tasks/http/authenticated_http_client.dart';
import '../google_tasks/http/retrying_http_client.dart';
import '../google_tasks/oauth/oauth_loopback_flow.dart';
import '../google_tasks/oauth/oauth_service.dart';
-import '../google_tasks/oauth/oauth_token_store.dart';
-import '../google_tasks/oauth/portal_encrypted_oauth_token_store.dart';
+import 'package:busymax/src/core/secrets/secret_store.dart';
+import 'package:busymax/src/core/secrets/portal_encrypted_secret_store.dart';
import '../google_calendar/google_calendar_api_client.dart';
import '../microsoft_calendar/microsoft_calendar_api_client.dart';
import '../microsoft_todo/api/microsoft_todo_api_client.dart';
-import '../microsoft_todo/api/microsoft_todo_google_tasks_adapter.dart';
+import '../microsoft_todo/api/microsoft_todo_task_remote_client.dart';
import '../microsoft_todo/oauth/microsoft_oauth_service.dart';
import '../platform/linux_window_service.dart';
-import '../task_providers/task_provider.dart';
+import 'package:busymax/src/providers/busy_provider.dart';
+import 'package:busymax/src/features/tasks/domain/task_capabilities.dart';
import '../schedule/schedule_commands.dart';
import '../schedule/schedule_repository.dart';
import 'app_router.dart';
@@ -66,6 +80,69 @@ final baseHttpClientProvider = Provider((ref) {
return client;
});
+final nextcloudLoginFlowV2Provider = Provider((ref) {
+ return NextcloudLoginFlowV2(client: ref.watch(baseHttpClientProvider));
+});
+
+final davAccountOnboardingServiceProvider =
+ Provider((ref) {
+ final client = ref.watch(baseHttpClientProvider);
+ return DavAccountOnboardingService(
+ database: ref.watch(databaseProvider),
+ secretStore: ref.watch(secretStoreProvider),
+ accountsRepository: ref.watch(accountsRepositoryProvider),
+ nextcloudLoginFlow: ref.watch(nextcloudLoginFlowV2Provider),
+ discover:
+ ({
+ required accountId,
+ required provider,
+ required accountAuthority,
+ required credential,
+ cancellationToken,
+ }) {
+ final profile = davProviderProfile(
+ provider,
+ nextcloudServer: provider == BusyProvider.nextcloud
+ ? accountAuthority
+ : null,
+ );
+ final transport = DavHttpTransport(
+ client: client,
+ profile: profile,
+ accountAuthority: accountAuthority,
+ );
+ return DavDiscoveryService(
+ transport: transport,
+ profile: profile,
+ accountAuthority: accountAuthority,
+ accountId: accountId,
+ credential: credential,
+ ).discover(
+ correlationId: const Uuid().v4(),
+ cancellationToken: cancellationToken,
+ );
+ },
+ nextcloudCredentialRevoker:
+ ({required accountId, required credential}) {
+ final profile = davProviderProfile(
+ BusyProvider.nextcloud,
+ nextcloudServer: credential.canonicalServer,
+ );
+ return NextcloudAppPasswordRevoker(
+ transport: DavHttpTransport(
+ client: client,
+ profile: profile,
+ accountAuthority: credential.canonicalServer,
+ ),
+ ).revoke(
+ accountId: accountId,
+ credential: credential,
+ correlationId: const Uuid().v4(),
+ );
+ },
+ );
+ });
+
final retryingHttpClientProvider = Provider((ref) {
return RetryingHttpClient(inner: ref.watch(baseHttpClientProvider));
});
@@ -80,20 +157,20 @@ final feedbackSubmissionServiceProvider = Provider((
);
});
-final oAuthTokenStoreProvider = Provider((ref) {
+final secretStoreProvider = Provider((ref) {
if (Platform.isLinux && (Platform.environment['SNAP']?.isNotEmpty ?? false)) {
// flutter_secure_storage_linux warms up direct libsecret first, so
// SECRET_BACKEND=file cannot avoid a locked keyring inside strict snaps.
- return PortalEncryptedOAuthTokenStore();
+ return PortalEncryptedSecretStore();
}
- return SecureOAuthTokenStore(ref.watch(secureStorageProvider));
+ return SecureSecretStore(ref.watch(secureStorageProvider));
});
final applicationOAuthServiceProvider = Provider((ref) {
return OAuthService(
config: ref.watch(buildConfigProvider),
httpClient: ref.watch(baseHttpClientProvider),
- tokenStore: ref.watch(oAuthTokenStoreProvider),
+ tokenStore: ref.watch(secretStoreProvider),
loopbackFlow: OAuthLoopbackFlow(),
);
});
@@ -108,7 +185,7 @@ final microsoftOAuthServiceProvider = Provider((ref) {
return MicrosoftOAuthService(
config: ref.watch(buildConfigProvider),
httpClient: ref.watch(baseHttpClientProvider),
- tokenStore: ref.watch(oAuthTokenStoreProvider),
+ tokenStore: ref.watch(secretStoreProvider),
loopbackFlow: OAuthLoopbackFlow(),
);
});
@@ -173,6 +250,49 @@ final accountManagementStreamProvider = StreamProvider>((
return ref.watch(accountsRepositoryProvider).watchVisibleAccounts();
});
+final davSettingsRepositoryProvider = Provider((ref) {
+ return DavSettingsRepository(
+ database: ref.watch(databaseProvider),
+ onVisibilityChanged: (_) =>
+ ref.read(notificationSchedulerProvider).checkNow(),
+ );
+});
+
+final davCollectionsStreamProvider =
+ StreamProvider>((ref) {
+ return ref.watch(davSettingsRepositoryProvider).watchCollections();
+ });
+
+final davConflictRepositoryProvider = Provider((ref) {
+ return DavConflictRepository(database: ref.watch(databaseProvider));
+});
+
+final davConflictsStreamProvider = StreamProvider>((
+ ref,
+) {
+ return ref.watch(davConflictRepositoryProvider).watchUnresolved();
+});
+
+final davConflictResolutionServiceProvider =
+ Provider((ref) {
+ final database = ref.watch(databaseProvider);
+ return DavConflictResolutionService(
+ database: database,
+ pendingQueue: DavPendingOperationQueue(database: database),
+ );
+ });
+
+final davTaskCollectionCapabilitiesProvider =
+ FutureProvider.family<
+ TaskCollectionCapabilities?,
+ ({String accountId, String taskListId})
+ >((ref, key) async {
+ final collection = await ref
+ .watch(davSettingsRepositoryProvider)
+ .collectionByTaskListId(key.accountId, key.taskListId);
+ return collection?.taskCapabilities;
+ });
+
final selectedAccountIdProvider = StateProvider((ref) => null);
final selectedAccountProvider = Provider((ref) {
@@ -191,17 +311,18 @@ final selectedAccountProvider = Provider((ref) {
return null;
});
-final selectedAccountCapabilitiesProvider = Provider((
- ref,
-) {
- final account = ref.watch(selectedAccountProvider);
- return capabilitiesForProvider(account?.provider ?? TaskProvider.google);
-});
+final selectedAccountCapabilitiesProvider =
+ Provider((ref) {
+ final account = ref.watch(selectedAccountProvider);
+ return account == null
+ ? noTaskCollectionCapabilities
+ : adapterDefaultTaskCapabilities(account.provider);
+ });
final localTimeZoneProvider = Provider((ref) => localIanaTimeZone());
final googleTasksApiClientForAccountProvider =
- Provider.family((ref, accountId) {
+ Provider.family((ref, accountId) {
final config = ref.watch(buildConfigProvider);
return GoogleTasksRestApiClient(
httpClient: ref.watch(retryingHttpClientProvider),
@@ -261,16 +382,16 @@ final microsoftCalendarApiClientForAccountProvider =
);
});
-final microsoftAsGoogleTasksApiClientForAccountProvider =
- Provider.family((ref, accountId) {
- return MicrosoftTodoGoogleTasksAdapter(
+final microsoftTodoTaskRemoteClientForAccountProvider =
+ Provider.family((ref, accountId) {
+ return MicrosoftTodoTaskRemoteClient(
client: ref.watch(microsoftTodoApiClientForAccountProvider(accountId)),
defaultTimeZone: ref.watch(localTimeZoneProvider),
);
});
final taskRemoteApiClientForAccountProvider =
- Provider.family((ref, accountId) {
+ Provider.family((ref, accountId) {
final accounts = ref.watch(accountsStreamProvider).valueOrNull;
AccountEntity? account;
for (final candidate in accounts ?? const []) {
@@ -283,35 +404,40 @@ final taskRemoteApiClientForAccountProvider =
return null;
}
return switch (account.provider) {
- TaskProvider.microsoft => ref.watch(
- microsoftAsGoogleTasksApiClientForAccountProvider(accountId),
+ BusyProvider.microsoft => ref.watch(
+ microsoftTodoTaskRemoteClientForAccountProvider(accountId),
),
- TaskProvider.google => ref.watch(
+ BusyProvider.google => ref.watch(
googleTasksApiClientForAccountProvider(accountId),
),
+ BusyProvider.appleICloud || BusyProvider.nextcloud => null,
};
});
typedef SyncEngineForAccountFactory =
- SyncEngine Function(String accountId, TaskProvider provider);
+ SyncEngine Function(String accountId, BusyProvider provider);
final syncEngineForAccountFactoryProvider =
Provider((ref) {
return (accountId, provider) {
final apiClient = switch (provider) {
- TaskProvider.microsoft => ref.read(
- microsoftAsGoogleTasksApiClientForAccountProvider(accountId),
+ BusyProvider.microsoft => ref.read(
+ microsoftTodoTaskRemoteClientForAccountProvider(accountId),
),
- TaskProvider.google => ref.read(
+ BusyProvider.google => ref.read(
googleTasksApiClientForAccountProvider(accountId),
),
+ BusyProvider.appleICloud ||
+ BusyProvider.nextcloud => throw StateError(
+ 'DAV task accounts must use DavSynchronizationEngine.',
+ ),
};
return SyncEngine(
database: ref.read(databaseProvider),
apiClient: apiClient,
accountId: accountId,
- fullRefreshOnly: provider == TaskProvider.microsoft,
+ fullRefreshOnly: provider == BusyProvider.microsoft,
onConflictBlocked: ref
.read(desktopNotificationServiceProvider)
.notifyConflict,
@@ -320,18 +446,22 @@ final syncEngineForAccountFactoryProvider =
});
typedef CalendarSyncEngineForAccountFactory =
- CalendarSyncEngine Function(String accountId, TaskProvider provider);
+ CalendarSyncEngine Function(String accountId, BusyProvider provider);
final calendarSyncEngineForAccountFactoryProvider =
Provider((ref) {
return (accountId, provider) {
final client = switch (provider) {
- TaskProvider.microsoft => ref.read(
+ BusyProvider.microsoft => ref.read(
microsoftCalendarApiClientForAccountProvider(accountId),
),
- TaskProvider.google => ref.read(
+ BusyProvider.google => ref.read(
googleCalendarApiClientForAccountProvider(accountId),
),
+ BusyProvider.appleICloud ||
+ BusyProvider.nextcloud => throw StateError(
+ 'DAV calendar accounts must use DavSynchronizationEngine.',
+ ),
};
return CalendarSyncEngine(
database: ref.read(databaseProvider),
@@ -346,10 +476,28 @@ final calendarSyncEngineForAccountFactoryProvider =
};
});
+typedef DavAccountSyncEngineFactory =
+ DavAccountSyncEngine Function(String accountId);
+
+final davAccountSyncEngineFactoryProvider =
+ Provider((ref) {
+ return (accountId) => DavAccountSyncEngine(
+ database: ref.read(databaseProvider),
+ secretStore: ref.read(secretStoreProvider),
+ httpClient: ref.read(baseHttpClientProvider),
+ accountId: accountId,
+ rebuildNotifications: (accountId, affectedObjectIds) =>
+ ref.read(notificationSchedulerProvider).checkNow(),
+ reportPendingMutationFailure: (accountId, error) => ref
+ .read(desktopNotificationServiceProvider)
+ .notifySyncFailure(error.safeMessage),
+ );
+ });
+
final accountSyncOperationsProvider = Provider((ref) {
final accountsRepository = ref.watch(accountsRepositoryProvider);
- Future providerForAccount(String accountId) async {
+ Future providerForAccount(String accountId) async {
final account = await accountsRepository.accountById(accountId);
if (account == null) {
throw StateError('Account $accountId is unavailable.');
@@ -357,8 +505,18 @@ final accountSyncOperationsProvider = Provider((ref) {
return account.provider;
}
- return DelegatingAccountSyncOperations(
- syncTasks: (accountId, {required full}) async {
+ return RoutingAccountSyncOperations(
+ usesDav: (accountId) async {
+ final provider = await providerForAccount(accountId);
+ return provider == BusyProvider.appleICloud ||
+ provider == BusyProvider.nextcloud;
+ },
+ syncDav: (accountId, {required full}) async {
+ await ref
+ .read(davAccountSyncEngineFactoryProvider)(accountId)
+ .synchronize(full: full);
+ },
+ syncTasksRest: (accountId, {required full}) async {
final provider = await providerForAccount(accountId);
final engine = ref.read(syncEngineForAccountFactoryProvider)(
accountId,
@@ -370,7 +528,7 @@ final accountSyncOperationsProvider = Provider((ref) {
await engine.incrementalSync();
}
},
- syncCalendar: (accountId, {required full}) async {
+ syncCalendarRest: (accountId, {required full}) async {
final provider = await providerForAccount(accountId);
final engine = ref.read(calendarSyncEngineForAccountFactoryProvider)(
accountId,
@@ -418,7 +576,7 @@ final allAccountsSyncRunnerProvider = Provider((ref) {
return runAllSignedInAccountSync(
listSignedInAccounts: ref
.read(accountsRepositoryProvider)
- .listSignedInAccounts,
+ .listSyncEligibleAccounts,
syncAccount: syncAccount,
onSyncFailure: ref
.read(desktopNotificationServiceProvider)
@@ -466,7 +624,7 @@ final Provider activeAccountProvider = Provider((ref) {
return session.isSignedIn ? session.accountId : null;
});
-final googleTasksApiClientProvider = Provider((ref) {
+final googleTasksApiClientProvider = Provider((ref) {
final accountId = ref.watch(activeAccountProvider);
if (accountId == null) {
return null;
@@ -474,6 +632,19 @@ final googleTasksApiClientProvider = Provider((ref) {
return ref.watch(taskRemoteApiClientForAccountProvider(accountId));
});
+final davTaskListMutationClientForAccountProvider =
+ Provider.family((ref, accountId) {
+ return DavTaskListMutationService(
+ database: ref.watch(databaseProvider),
+ secretStore: ref.watch(secretStoreProvider),
+ httpClient: ref.watch(baseHttpClientProvider),
+ accountId: accountId,
+ refreshAfterMutation: () => ref
+ .read(accountSyncOperationsProvider)
+ .syncAccount(accountId, full: true),
+ );
+ });
+
final taskListsRepositoryProvider = Provider((ref) {
final accountId = ref.watch(activeAccountProvider);
if (accountId == null) {
@@ -483,6 +654,9 @@ final taskListsRepositoryProvider = Provider((ref) {
database: ref.watch(databaseProvider),
accountId: accountId,
apiClient: ref.watch(googleTasksApiClientProvider),
+ davMutationClient: ref.watch(
+ davTaskListMutationClientForAccountProvider(accountId),
+ ),
onMutationQueued: ref.watch(pendingMutationSyncRequesterProvider)?.request,
);
});
@@ -492,6 +666,13 @@ final taskListsRepositoryForAccountProvider =
return TaskListsRepository(
database: ref.watch(databaseProvider),
accountId: accountId,
+ apiClient: ref.watch(taskRemoteApiClientForAccountProvider(accountId)),
+ davMutationClient: ref.watch(
+ davTaskListMutationClientForAccountProvider(accountId),
+ ),
+ onMutationQueued: ref
+ .watch(pendingMutationSyncRequesterForAccountProvider(accountId))
+ .request,
);
});
@@ -535,7 +716,7 @@ final Provider syncEngineProvider = Provider((ref) {
database: ref.watch(databaseProvider),
apiClient: apiClient,
accountId: accountId,
- fullRefreshOnly: account!.provider == TaskProvider.microsoft,
+ fullRefreshOnly: account!.provider == BusyProvider.microsoft,
onConflictBlocked: ref
.watch(desktopNotificationServiceProvider)
.notifyConflict,
diff --git a/lib/src/app/busymax_design.dart b/lib/src/app/busymax_design.dart
index 5411d1c..1f27c0f 100644
--- a/lib/src/app/busymax_design.dart
+++ b/lib/src/app/busymax_design.dart
@@ -3053,38 +3053,58 @@ class BusyMaxEmptyState extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final colorScheme = Theme.of(context).colorScheme;
+ final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
+ final hasMessage = message != null && message!.isNotEmpty;
return Center(
- child: Padding(
+ child: SingleChildScrollView(
padding: const EdgeInsets.all(BusyMaxSpacing.xl),
child: ConstrainedBox(
- constraints: const BoxConstraints(maxWidth: 420),
- child: YaruInfoBox(
- yaruInfoType: YaruInfoType.information,
- color: colorScheme.onSurfaceVariant,
- icon: Icon(icon),
- title: Text(title),
+ constraints: const BoxConstraints(maxWidth: 440),
+ child: Semantics(
+ container: true,
child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.center,
children: [
- if (message != null && message!.isNotEmpty)
+ ExcludeSemantics(
+ child: Icon(
+ icon,
+ size: 56,
+ color: colorScheme.onSurfaceVariant,
+ ),
+ ),
+ const SizedBox(height: BusyMaxSpacing.lg),
+ Semantics(
+ header: true,
+ child: Text(
+ title,
+ textAlign: TextAlign.center,
+ style: theme.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ if (hasMessage) ...[
+ const SizedBox(height: BusyMaxSpacing.sm),
Text(
message!,
- style: Theme.of(context).textTheme.bodyMedium?.copyWith(
+ textAlign: TextAlign.center,
+ style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
+ ],
if (actions.isNotEmpty) ...[
- if (message != null && message!.isNotEmpty)
- const SizedBox(height: BusyMaxSpacing.lg),
+ const SizedBox(height: BusyMaxSpacing.lg),
Wrap(
+ alignment: WrapAlignment.center,
+ crossAxisAlignment: WrapCrossAlignment.center,
spacing: BusyMaxSpacing.sm,
runSpacing: BusyMaxSpacing.sm,
children: actions,
),
],
- if ((message == null || message!.isEmpty) && actions.isEmpty)
- const SizedBox.shrink(),
],
),
),
diff --git a/lib/src/app/busymax_dialogs.dart b/lib/src/app/busymax_dialogs.dart
index 1acfd79..5d1fdef 100644
--- a/lib/src/app/busymax_dialogs.dart
+++ b/lib/src/app/busymax_dialogs.dart
@@ -147,12 +147,14 @@ Future showBusyMaxModalEditorDialog(
BuildContext context, {
required WidgetBuilder builder,
LinuxHeaderBarService? headerBarService,
+ Color? barrierColor,
double maxWidth = BusyMaxSizes.compactDetailsWidth,
double? maxHeight = 760,
}) async {
return showBusyMaxModalDialog(
context,
headerBarService: headerBarService,
+ barrierColor: barrierColor,
barrierDismissible: false,
builder: (dialogContext) {
return BusyMaxModalEditorSurface(
diff --git a/lib/src/calendar_providers/calendar_colors.dart b/lib/src/calendar_providers/calendar_colors.dart
index 3766468..21af0b2 100644
--- a/lib/src/calendar_providers/calendar_colors.dart
+++ b/lib/src/calendar_providers/calendar_colors.dart
@@ -1,4 +1,4 @@
-import '../task_providers/task_provider.dart';
+import 'package:busymax/src/providers/busy_provider.dart';
String? calendarSourceBackgroundColorHex({
required BusyProvider provider,
@@ -9,7 +9,7 @@ String? calendarSourceBackgroundColorHex({
if (explicitColor != null) {
return explicitColor;
}
- if (provider == TaskProvider.microsoft) {
+ if (provider == BusyProvider.microsoft) {
return microsoftCalendarColorHex(colorId);
}
return null;
diff --git a/lib/src/calendar_providers/calendar_sync_dto.dart b/lib/src/calendar_providers/calendar_sync_dto.dart
index 277694d..911ed3c 100644
--- a/lib/src/calendar_providers/calendar_sync_dto.dart
+++ b/lib/src/calendar_providers/calendar_sync_dto.dart
@@ -1,4 +1,4 @@
-import '../task_providers/task_provider.dart';
+import 'package:busymax/src/providers/busy_provider.dart';
class CalendarSourceDto {
const CalendarSourceDto({
diff --git a/lib/src/calendar_providers/cloud_calendar_client.dart b/lib/src/calendar_providers/cloud_calendar_client.dart
index e365ac3..fe1c90b 100644
--- a/lib/src/calendar_providers/cloud_calendar_client.dart
+++ b/lib/src/calendar_providers/cloud_calendar_client.dart
@@ -1,4 +1,4 @@
-import '../task_providers/task_provider.dart';
+import 'package:busymax/src/providers/busy_provider.dart';
import 'calendar_mutation.dart';
import 'calendar_provider_capabilities.dart';
import 'calendar_sync_dto.dart';
diff --git a/lib/src/config/build_config.dart b/lib/src/config/build_config.dart
index 10227b0..df0974d 100644
--- a/lib/src/config/build_config.dart
+++ b/lib/src/config/build_config.dart
@@ -93,8 +93,13 @@ class BuildConfig {
useFakeProviderData || googleOAuthClientId.trim().isNotEmpty;
bool get hasMicrosoftOAuthClientId =>
!useFakeProviderData && microsoftOAuthClientId.trim().isNotEmpty;
+ bool get hasAppleICloudProvider => !useFakeProviderData;
+ bool get hasNextcloudProvider => !useFakeProviderData;
bool get hasAnyProviderConfigured =>
- hasGoogleOAuthClientId || hasMicrosoftOAuthClientId;
+ hasGoogleOAuthClientId ||
+ hasMicrosoftOAuthClientId ||
+ hasAppleICloudProvider ||
+ hasNextcloudProvider;
String get missingClientIdMessage {
if (kReleaseMode) {
diff --git a/lib/src/google_tasks/oauth/oauth_models.dart b/lib/src/core/auth/oauth_models.dart
similarity index 97%
rename from lib/src/google_tasks/oauth/oauth_models.dart
rename to lib/src/core/auth/oauth_models.dart
index 6af132c..5a27de5 100644
--- a/lib/src/google_tasks/oauth/oauth_models.dart
+++ b/lib/src/core/auth/oauth_models.dart
@@ -1,3 +1,4 @@
+/// OAuth token record shared by the Google and Microsoft adapters.
class OAuthTokenSet {
const OAuthTokenSet({
required this.accessToken,
diff --git a/lib/src/core/logging/redacting_logger.dart b/lib/src/core/logging/redacting_logger.dart
index 8261685..33d50d4 100644
--- a/lib/src/core/logging/redacting_logger.dart
+++ b/lib/src/core/logging/redacting_logger.dart
@@ -3,41 +3,76 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
-final _sensitivePatterns = [
- RegExp(r'Bearer\s+[A-Za-z0-9._~+/=-]+', caseSensitive: false),
- RegExp(r'(^|[?&\s])access_token=[^&\s]+', caseSensitive: false),
- RegExp(r'(^|[?&\s])refresh_token=[^&\s]+', caseSensitive: false),
- // RFC 7009 uses the generic `token` field. Restrict this pattern to URL
- // query syntax so ordinary prose such as "token=value" remains readable.
- RegExp(r'([?&])token=[^&\s]+', caseSensitive: false),
- RegExp(r'(^|[?&\s])client_secret=[^&\s]+', caseSensitive: false),
- RegExp(r'"client_secret"\s*:\s*"[^"]*"', caseSensitive: false),
- RegExp(r'client_secret\s*:\s*[^,\n\s]+', caseSensitive: false),
- RegExp(r'(^|[?&\s])code_verifier=[^&\s]+', caseSensitive: false),
- RegExp(r'(^|[?&\s])code=[^&\s]+', caseSensitive: false),
- RegExp(r'Authorization:\s*[^,\n]+', caseSensitive: false),
-];
-
String redactForLog(Object? value) {
var text = value?.toString() ?? '';
- for (final pattern in _sensitivePatterns) {
- text = text.replaceAllMapped(pattern, (match) {
- final source = match.group(0) ?? '';
- if (source.contains('=')) {
- return '${source.split('=').first}=[REDACTED]';
- }
- if (source.trimLeft().startsWith('"')) {
- return '"client_secret":"[REDACTED]"';
- }
- if (source.toLowerCase().contains('client_secret')) {
- return 'client_secret: [REDACTED]';
- }
- if (source.toLowerCase().startsWith('authorization:')) {
- return 'Authorization: [REDACTED]';
- }
- return 'Bearer [REDACTED]';
- });
- }
+ text = text.replaceAllMapped(
+ RegExp(r'\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+', caseSensitive: false),
+ (match) => '${match.group(1)} [REDACTED]',
+ );
+ text = text.replaceAllMapped(
+ RegExp(
+ r'\bAuthorization\s*[:=]\s*(?:(Bearer|Basic)\s+)?[^,\r\n}]+',
+ caseSensitive: false,
+ ),
+ (match) =>
+ 'Authorization: ${match.group(1) == null ? '' : '${match.group(1)} '}'
+ '[REDACTED]',
+ );
+ text = text.replaceAllMapped(
+ RegExp(r'\b(?:Set-)?Cookie\s*[:=]\s*[^\r\n]+', caseSensitive: false),
+ (match) =>
+ '${(match.group(0) ?? '').split(RegExp(r'[:=]')).first}: '
+ '[REDACTED]',
+ );
+ text = text.replaceAllMapped(
+ RegExp(r'(https?://)[^/@\s]+@', caseSensitive: false),
+ (match) => '${match.group(1)}[REDACTED]@',
+ );
+ text = text.replaceAllMapped(
+ RegExp(
+ r'(^|[\s])((?:access|refresh|id)[_-]?token|code|code[_-]?verifier|client[_-]?secret|password|app[_-]?password|app[_-]?specific[_-]?password|poll[_-]?token)=([^&\s]+)',
+ caseSensitive: false,
+ multiLine: true,
+ ),
+ (match) => '${match.group(1)}${match.group(2)}=[REDACTED]',
+ );
+ text = text.replaceAllMapped(
+ RegExp(
+ r'([?&])((?:access|refresh|id)[_-]?token|token|code|code_verifier|client_secret|password|app[_-]?password|ticket|session|key)=([^\s]+)',
+ caseSensitive: false,
+ ),
+ (match) => '${match.group(1)}${match.group(2)}=[REDACTED]',
+ );
+ text = text.replaceAllMapped(
+ RegExp(
+ r'"(accessToken|refreshToken|idToken|token|clientSecret|client_secret|codeVerifier|appPassword|appSpecificPassword|password|pollToken|login|loginUrl|requestBody|responseBody|body|cookie)"\s*:\s*(?:"(?:\\.|[^"])*"|[^,}\s]+)',
+ caseSensitive: false,
+ ),
+ (match) => '"${match.group(1)}":"[REDACTED]"',
+ );
+ text = text.replaceAllMapped(
+ RegExp(
+ r'\b(access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|code[_-]?verifier|app[_-]?password|app[_-]?specific[_-]?password|password|poll[_-]?token|loginUrl|requestBody|responseBody|cookie)\s*([:=])\s*(?:"(?:\\.|[^"])*"|\x27[^\x27]*\x27|[^,}\s]+)',
+ caseSensitive: false,
+ ),
+ (match) =>
+ '${match.group(1)}${match.group(2)}'
+ '${match.group(2) == '=' ? '' : ' '}[REDACTED]',
+ );
+ text = text.replaceAllMapped(
+ RegExp(
+ r'\b(href|requestUri|objectUri|collectionUri)\s*([:=])\s*[^,}\s]+',
+ caseSensitive: false,
+ ),
+ (match) => '${match.group(1)}${match.group(2)} [REDACTED]',
+ );
+ text = text.replaceAllMapped(
+ RegExp(
+ r'\b(SUMMARY|DESCRIPTION|LOCATION|ATTENDEE|ORGANIZER|COMMENT|CONTACT|ATTACH|X-ALT-DESC)(?:;[^:\r\n]*)?:[^\r\n]*',
+ caseSensitive: false,
+ ),
+ (match) => '${match.group(1)}:[REDACTED]',
+ );
return text;
}
diff --git a/lib/src/google_tasks/oauth/portal_encrypted_oauth_token_store.dart b/lib/src/core/secrets/portal_encrypted_secret_store.dart
similarity index 68%
rename from lib/src/google_tasks/oauth/portal_encrypted_oauth_token_store.dart
rename to lib/src/core/secrets/portal_encrypted_secret_store.dart
index 7a111f6..a807f6e 100644
--- a/lib/src/google_tasks/oauth/portal_encrypted_oauth_token_store.dart
+++ b/lib/src/core/secrets/portal_encrypted_secret_store.dart
@@ -7,22 +7,24 @@ import 'package:cryptography/cryptography.dart';
import 'package:dbus/dbus.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
+import 'package:posix/posix.dart' show chmod;
-import '../../core/logging/redacting_logger.dart';
-import 'oauth_models.dart';
-import 'oauth_token_store.dart';
+import '../../providers/busy_provider.dart';
+import '../auth/oauth_models.dart';
+import '../logging/redacting_logger.dart';
+import 'secret_store.dart';
const _encryptedTokenStoreVersion = 1;
-class PortalEncryptedOAuthTokenStore implements OAuthTokenStore {
- PortalEncryptedOAuthTokenStore({
+class PortalEncryptedSecretStore implements SecretStore {
+ PortalEncryptedSecretStore({
SecretPortalClient? portalClient,
File? storageFile,
RedactingLogger? logger,
}) : _portalClient = portalClient ?? XdgSecretPortalClient(),
_storageFile = storageFile ?? _defaultStorageFile(),
_logger =
- logger ?? RedactingLogger(Logger('PortalEncryptedOAuthTokenStore'));
+ logger ?? RedactingLogger(Logger('PortalEncryptedSecretStore'));
final SecretPortalClient _portalClient;
final File _storageFile;
@@ -32,53 +34,45 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore {
PortalSecret? _cachedSecret;
var _loggedRuntime = false;
- static const _activeAccountKey = SecureOAuthTokenStore.activeAccountKey;
+ static const _activeAccountKey = SecureSecretStore.activeAccountKey;
+ static const _legacyActiveAccountKey =
+ SecureSecretStore.legacyActiveAccountKey;
static const _kdfInfo = 'io.busystack.busymax.oauth-token-store.v1';
@override
- Future readActiveAccountId() => _read(_activeAccountKey);
+ Future readActiveAccountId() async {
+ final current = await _read(_activeAccountKey);
+ if (current != null) {
+ return current;
+ }
+ final legacy = await _read(_legacyActiveAccountKey);
+ if (legacy == null) {
+ return null;
+ }
+ await _write(_activeAccountKey, legacy);
+ if (await _read(_activeAccountKey) != legacy) {
+ throw const SecretStoreException(
+ 'SecretStoreMigrationVerificationFailed',
+ 'The active account secret migration could not be verified.',
+ );
+ }
+ await _delete(_legacyActiveAccountKey);
+ return legacy;
+ }
@override
- Future readTokenSet(String accountId) async {
- final accessToken = await _read(_key(accountId, 'access_token'));
- final expiresAtText = await _read(_key(accountId, 'expires_at_utc'));
- if (accessToken == null || expiresAtText == null) {
+ Future readCredential(String accountId) async {
+ final serialized = await _read(_credentialKey(accountId));
+ if (serialized == null) {
return null;
}
-
- final refreshToken = await _read(_key(accountId, 'refresh_token'));
- final tokenType = await _read(_key(accountId, 'token_type'));
- final scopeText = await _read(_key(accountId, 'scope'));
- final idToken = await _read(_key(accountId, 'id_token'));
-
- return OAuthTokenSet(
- accessToken: accessToken,
- refreshToken: refreshToken,
- idToken: idToken,
- expiresAtUtc: DateTime.parse(expiresAtText).toUtc(),
- tokenType: tokenType ?? 'Bearer',
- scopes: (scopeText ?? '')
- .split(RegExp(r'\s+'))
- .where((scope) => scope.isNotEmpty)
- .toSet(),
- );
+ return _decodePortalCredential(serialized);
}
@override
- Future saveTokenSet(String accountId, OAuthTokenSet tokenSet) async {
+ Future saveCredential(String accountId, SecretRecord credential) async {
final values = await _readAll('write');
- values[_key(accountId, 'access_token')] = tokenSet.accessToken;
- if (tokenSet.refreshToken != null) {
- values[_key(accountId, 'refresh_token')] = tokenSet.refreshToken!;
- }
- if (tokenSet.idToken != null) {
- values[_key(accountId, 'id_token')] = tokenSet.idToken!;
- }
- values[_key(accountId, 'expires_at_utc')] = tokenSet.expiresAtUtc
- .toUtc()
- .toIso8601String();
- values[_key(accountId, 'token_type')] = tokenSet.tokenType;
- values[_key(accountId, 'scope')] = tokenSet.scopes.join(' ');
+ values[_credentialKey(accountId)] = jsonEncode(credential.toJson());
await _writeAll(values, 'write');
}
@@ -88,21 +82,62 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore {
}
@override
- Future clearTokenSet(String accountId) async {
+ Future deleteCredential(String accountId) async {
final values = await _readAll('delete');
- final beforeLength = values.length;
- values.remove(_key(accountId, 'access_token'));
- values.remove(_key(accountId, 'refresh_token'));
- values.remove(_key(accountId, 'id_token'));
- values.remove(_key(accountId, 'expires_at_utc'));
- values.remove(_key(accountId, 'token_type'));
- values.remove(_key(accountId, 'scope'));
- if (values.length == beforeLength) {
+ if (values.remove(_credentialKey(accountId)) == null) {
return;
}
await _writeAll(values, 'delete');
}
+ @override
+ Future migrateLegacyOAuthCredential(
+ String accountId,
+ BusyProvider provider,
+ ) async {
+ if (provider != BusyProvider.google && provider != BusyProvider.microsoft) {
+ return false;
+ }
+ final values = await _readAll('migrate');
+ if (values.containsKey(_credentialKey(accountId))) {
+ return false;
+ }
+ final accessToken = values[_legacyKey(accountId, 'access_token')];
+ final expiresAt = values[_legacyKey(accountId, 'expires_at_utc')];
+ if (accessToken == null || expiresAt == null) {
+ return false;
+ }
+ final record = OAuthSecretRecord(
+ provider: provider,
+ tokenSet: OAuthTokenSet(
+ accessToken: accessToken,
+ refreshToken: values[_legacyKey(accountId, 'refresh_token')],
+ idToken: values[_legacyKey(accountId, 'id_token')],
+ expiresAtUtc: DateTime.parse(expiresAt).toUtc(),
+ tokenType: values[_legacyKey(accountId, 'token_type')] ?? 'Bearer',
+ scopes: (values[_legacyKey(accountId, 'scope')] ?? '')
+ .split(RegExp(r'\s+'))
+ .where((scope) => scope.isNotEmpty)
+ .toSet(),
+ ),
+ );
+ values[_credentialKey(accountId)] = jsonEncode(record.toJson());
+ await _writeAll(values, 'migrate-write');
+ final verified = await readOAuthTokenSet(accountId, provider);
+ if (verified == null || verified.accessToken != accessToken) {
+ throw const SecretStoreException(
+ 'SecretStoreMigrationVerificationFailed',
+ 'The OAuth credential migration could not be verified.',
+ );
+ }
+ final migrated = await _readAll('migrate-delete');
+ for (final name in _legacyOAuthFieldNames) {
+ migrated.remove(_legacyKey(accountId, name));
+ }
+ await _writeAll(migrated, 'migrate-delete');
+ return true;
+ }
+
@override
Future clearActiveAccount() {
return _delete(_activeAccountKey);
@@ -135,6 +170,7 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore {
}
try {
+ await _restrictExistingStoragePermissions();
final envelope = _asStringObjectMap(
jsonDecode(await _storageFile.readAsString()),
);
@@ -152,7 +188,7 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore {
final clearBytes = await _cipher.decrypt(box, secretKey: key);
return _asStringMap(jsonDecode(utf8.decode(clearBytes)));
} on Object catch (error) {
- if (error is OAuthException) {
+ if (error is SecretStoreException) {
rethrow;
}
throw _storageException(operation, error);
@@ -163,6 +199,7 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore {
_logRuntime();
try {
await _storageFile.parent.create(recursive: true);
+ _restrictPermissions(_storageFile.parent.path, '700');
final existing = await _readEnvelopeIfPresent();
final salt = existing == null
? _randomBytes(16)
@@ -190,10 +227,24 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore {
'portal_token': secret.token,
};
final tempFile = File('${_storageFile.path}.tmp');
+ final tempType = await FileSystemEntity.type(
+ tempFile.path,
+ followLinks: false,
+ );
+ if (tempType == FileSystemEntityType.notFound) {
+ await tempFile.create(exclusive: true);
+ } else if (tempType != FileSystemEntityType.file) {
+ throw FileSystemException(
+ 'The encrypted credential temporary path is not a regular file.',
+ tempFile.path,
+ );
+ }
+ _restrictPermissions(tempFile.path, '600');
await tempFile.writeAsString(jsonEncode(envelope), flush: true);
await tempFile.rename(_storageFile.path);
+ _restrictPermissions(_storageFile.path, '600');
} on Object catch (error) {
- if (error is OAuthException) {
+ if (error is SecretStoreException) {
rethrow;
}
throw _storageException(operation, error);
@@ -204,9 +255,31 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore {
if (!await _storageFile.exists()) {
return null;
}
+ await _restrictExistingStoragePermissions();
return _asStringObjectMap(jsonDecode(await _storageFile.readAsString()));
}
+ Future _restrictExistingStoragePermissions() async {
+ final type = await FileSystemEntity.type(
+ _storageFile.path,
+ followLinks: false,
+ );
+ if (type != FileSystemEntityType.file) {
+ throw FileSystemException(
+ 'The encrypted credential path is not a regular file.',
+ _storageFile.path,
+ );
+ }
+ _restrictPermissions(_storageFile.parent.path, '700');
+ _restrictPermissions(_storageFile.path, '600');
+ }
+
+ void _restrictPermissions(String path, String permissions) {
+ if (Platform.isLinux || Platform.isMacOS) {
+ chmod(path, permissions);
+ }
+ }
+
Future _retrieveSecret(
String operation, {
String? token,
@@ -219,7 +292,7 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore {
final secret = await _portalClient.retrieveSecret(token: token);
_cachedSecret = secret;
_logger.info(
- 'Secure token storage portal retrieve succeeded: '
+ 'Secure credential storage portal retrieve succeeded: '
'operation=$operation snap=${_isRunningInSnap()} '
'secret_backend=${_secretBackendLabel()} has_portal_token=${secret.token != null}',
);
@@ -243,26 +316,28 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore {
}
_loggedRuntime = true;
_logger.info(
- 'Secure token storage runtime: backend=xdg-secret-portal-file '
+ 'Secure credential storage runtime: backend=xdg-secret-portal-file '
'snap=${_isRunningInSnap()} secret_backend=${_secretBackendLabel()}',
);
}
- OAuthException _storageException(String operation, Object error) {
+ SecretStoreException _storageException(String operation, Object error) {
_logger.warning(
- 'Secure token storage $operation failed: '
+ 'Secure credential storage $operation failed: '
'${sanitizedSecureStorageError(error)}',
);
- if (error is OAuthException) {
+ if (error is SecretStoreException) {
return error;
}
- return const OAuthException(
- 'OAuthSecureStorageUnavailable',
- secureTokenStorageUnavailableMessage,
+ return const SecretStoreException(
+ 'SecretStoreUnavailable',
+ secretStorageUnavailableMessage,
);
}
- String _key(String accountId, String name) =>
+ String _credentialKey(String accountId) => 'busymax.secret.$accountId.v1';
+
+ String _legacyKey(String accountId, String name) =>
'busymax.oauth.$accountId.$name';
}
@@ -447,6 +522,25 @@ Map _asStringMap(Object? value) {
});
}
+SecretRecord _decodePortalCredential(String serialized) {
+ try {
+ final decoded = jsonDecode(serialized);
+ if (decoded is! Map) {
+ throw const SecretStoreCorruptException(
+ 'The encrypted credential record is not a JSON object.',
+ );
+ }
+ return SecretRecord.fromJson(decoded.cast());
+ } on SecretStoreException {
+ rethrow;
+ } on Object catch (error) {
+ throw SecretStoreCorruptException(
+ 'The encrypted credential record could not be decoded '
+ '(${error.runtimeType}).',
+ );
+ }
+}
+
Map _asStringObjectMap(Object? value) {
if (value is! Map) {
throw const FormatException('Encrypted token store envelope is not a map.');
@@ -480,6 +574,15 @@ List _randomBytes(int length) {
return List.generate(length, (_) => random.nextInt(256));
}
+const _legacyOAuthFieldNames = [
+ 'access_token',
+ 'refresh_token',
+ 'id_token',
+ 'expires_at_utc',
+ 'token_type',
+ 'scope',
+];
+
String _requestToken() {
final bytes = _randomBytes(16);
final hex = bytes
diff --git a/lib/src/core/secrets/secret_store.dart b/lib/src/core/secrets/secret_store.dart
new file mode 100644
index 0000000..0e9d083
--- /dev/null
+++ b/lib/src/core/secrets/secret_store.dart
@@ -0,0 +1,516 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:flutter/services.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+import 'package:logging/logging.dart';
+
+import '../../providers/busy_provider.dart';
+import '../../providers/account_authority.dart';
+import '../auth/oauth_models.dart';
+import '../logging/redacting_logger.dart';
+
+const secretRecordSchemaVersion = 1;
+
+enum CredentialKind { oauth, appleAppSpecificPassword, nextcloudAppPassword }
+
+extension CredentialKindValue on CredentialKind {
+ String get storageValue => switch (this) {
+ CredentialKind.oauth => 'oauth',
+ CredentialKind.appleAppSpecificPassword => 'apple_app_specific_password',
+ CredentialKind.nextcloudAppPassword => 'nextcloud_app_password',
+ };
+}
+
+sealed class SecretRecord {
+ const SecretRecord({required this.provider, required this.kind});
+
+ final BusyProvider provider;
+ final CredentialKind kind;
+
+ Map toJson();
+
+ static SecretRecord fromJson(Map json) {
+ if (json['schemaVersion'] != secretRecordSchemaVersion) {
+ throw SecretStoreCorruptException(
+ 'Unsupported credential schema version ${json['schemaVersion']}.',
+ );
+ }
+ final provider = BusyProviderCodec.requireStorageValue(
+ json['provider']?.toString(),
+ );
+ return switch (json['kind']) {
+ 'oauth' => OAuthSecretRecord(
+ provider: provider,
+ tokenSet: OAuthTokenSet(
+ accessToken: _requiredSecretString(json, 'accessToken'),
+ refreshToken: _optionalSecretString(json, 'refreshToken'),
+ idToken: _optionalSecretString(json, 'idToken'),
+ expiresAtUtc: DateTime.parse(
+ _requiredSecretString(json, 'expiresAtUtc'),
+ ).toUtc(),
+ tokenType: _requiredSecretString(json, 'tokenType'),
+ scopes: _stringList(json['scopes']).toSet(),
+ ),
+ ),
+ 'apple_app_specific_password' => AppleICloudSecretRecord(
+ username: _requiredSecretString(json, 'username'),
+ appSpecificPassword: _requiredSecretString(json, 'appSpecificPassword'),
+ ),
+ 'nextcloud_app_password' => NextcloudSecretRecord(
+ canonicalServer: Uri.parse(
+ _requiredSecretString(json, 'canonicalServer'),
+ ),
+ loginName: _requiredSecretString(json, 'loginName'),
+ appPassword: _requiredSecretString(json, 'appPassword'),
+ ),
+ final Object? value => throw SecretStoreCorruptException(
+ 'Unsupported credential kind $value.',
+ ),
+ };
+ }
+
+ @override
+ String toString() =>
+ '$runtimeType(provider: ${provider.storageValue}, secret: [REDACTED])';
+}
+
+final class OAuthSecretRecord extends SecretRecord {
+ const OAuthSecretRecord({required super.provider, required this.tokenSet})
+ : assert(
+ provider == BusyProvider.google || provider == BusyProvider.microsoft,
+ ),
+ super(kind: CredentialKind.oauth);
+
+ final OAuthTokenSet tokenSet;
+
+ @override
+ Map toJson() => {
+ 'schemaVersion': secretRecordSchemaVersion,
+ 'kind': kind.storageValue,
+ 'provider': provider.storageValue,
+ 'accessToken': tokenSet.accessToken,
+ if (tokenSet.refreshToken != null) 'refreshToken': tokenSet.refreshToken,
+ if (tokenSet.idToken != null) 'idToken': tokenSet.idToken,
+ 'expiresAtUtc': tokenSet.expiresAtUtc.toUtc().toIso8601String(),
+ 'tokenType': tokenSet.tokenType,
+ 'scopes': tokenSet.scopes.toList()..sort(),
+ };
+}
+
+final class AppleICloudSecretRecord extends SecretRecord {
+ AppleICloudSecretRecord({
+ required String username,
+ required String appSpecificPassword,
+ }) : username = username.trim(),
+ appSpecificPassword = appSpecificPassword.trim(),
+ super(
+ provider: BusyProvider.appleICloud,
+ kind: CredentialKind.appleAppSpecificPassword,
+ ) {
+ if (this.username.isEmpty || this.appSpecificPassword.isEmpty) {
+ throw const SecretStoreCorruptException(
+ 'Apple iCloud credentials must not be empty.',
+ );
+ }
+ }
+
+ final String username;
+ final String appSpecificPassword;
+
+ @override
+ Map toJson() => {
+ 'schemaVersion': secretRecordSchemaVersion,
+ 'kind': kind.storageValue,
+ 'provider': provider.storageValue,
+ 'username': username,
+ 'appSpecificPassword': appSpecificPassword,
+ };
+}
+
+final class NextcloudSecretRecord extends SecretRecord {
+ NextcloudSecretRecord({
+ required Uri canonicalServer,
+ required String loginName,
+ required String appPassword,
+ }) : canonicalServer = Uri.parse(
+ normalizeNextcloudServerAuthority(canonicalServer.toString()),
+ ),
+ loginName = loginName.trim(),
+ appPassword = appPassword.trim(),
+ super(
+ provider: BusyProvider.nextcloud,
+ kind: CredentialKind.nextcloudAppPassword,
+ ) {
+ if (canonicalServer.scheme != 'https' ||
+ canonicalServer.host.isEmpty ||
+ canonicalServer.userInfo.isNotEmpty ||
+ this.loginName.isEmpty ||
+ this.appPassword.isEmpty) {
+ throw const SecretStoreCorruptException(
+ 'Nextcloud credentials contain an invalid server or empty value.',
+ );
+ }
+ }
+
+ final Uri canonicalServer;
+ final String loginName;
+ final String appPassword;
+
+ @override
+ Map toJson() => {
+ 'schemaVersion': secretRecordSchemaVersion,
+ 'kind': kind.storageValue,
+ 'provider': provider.storageValue,
+ 'canonicalServer': canonicalServer.toString(),
+ 'loginName': loginName,
+ 'appPassword': appPassword,
+ };
+}
+
+abstract interface class SecretStore {
+ Future readActiveAccountId();
+ Future setActiveAccountId(String accountId);
+ Future clearActiveAccount();
+ Future readCredential(String accountId);
+ Future saveCredential(String accountId, SecretRecord credential);
+ Future deleteCredential(String accountId);
+
+ /// Performs the one-time legacy OAuth key migration for an existing account.
+ /// Returns true only when legacy values were replaced and then deleted.
+ Future migrateLegacyOAuthCredential(
+ String accountId,
+ BusyProvider provider,
+ );
+}
+
+extension OAuthSecretStoreAccess on SecretStore {
+ Future readOAuthTokenSet(
+ String accountId,
+ BusyProvider expectedProvider,
+ ) async {
+ final credential = await readCredential(accountId);
+ if (credential == null) {
+ return null;
+ }
+ if (credential case OAuthSecretRecord(
+ provider: final provider,
+ tokenSet: final tokenSet,
+ ) when provider == expectedProvider) {
+ return tokenSet;
+ }
+ throw SecretStoreCredentialMismatchException(
+ accountId: accountId,
+ expectedProvider: expectedProvider,
+ actualProvider: credential.provider,
+ actualKind: credential.kind,
+ );
+ }
+
+ Future saveOAuthTokenSet(
+ String accountId,
+ BusyProvider provider,
+ OAuthTokenSet tokenSet,
+ ) {
+ return saveCredential(
+ accountId,
+ OAuthSecretRecord(provider: provider, tokenSet: tokenSet),
+ );
+ }
+}
+
+class SecureSecretStore implements SecretStore {
+ SecureSecretStore(this._storage, {RedactingLogger? logger})
+ : _logger = logger ?? RedactingLogger(Logger('SecureSecretStore'));
+
+ final FlutterSecureStorage _storage;
+ final RedactingLogger _logger;
+ var _loggedRuntime = false;
+
+ static const activeAccountKey = 'busymax.secret.active_account_id';
+ static const legacyActiveAccountKey = 'busymax.oauth.active_account_id';
+
+ @override
+ Future readActiveAccountId() async {
+ final current = await _read(activeAccountKey);
+ if (current != null) {
+ return current;
+ }
+ final legacy = await _read(legacyActiveAccountKey);
+ if (legacy == null) {
+ return null;
+ }
+ await _write(activeAccountKey, legacy);
+ if (await _read(activeAccountKey) != legacy) {
+ throw const SecretStoreException(
+ 'SecretStoreMigrationVerificationFailed',
+ 'The active account secret migration could not be verified.',
+ );
+ }
+ await _delete(legacyActiveAccountKey);
+ return legacy;
+ }
+
+ @override
+ Future readCredential(String accountId) async {
+ final serialized = await _read(_credentialKey(accountId));
+ if (serialized == null) {
+ return null;
+ }
+ return _decodeCredential(serialized);
+ }
+
+ @override
+ Future saveCredential(String accountId, SecretRecord credential) async {
+ await _write(_credentialKey(accountId), jsonEncode(credential.toJson()));
+ }
+
+ @override
+ Future setActiveAccountId(String accountId) {
+ return _write(activeAccountKey, accountId);
+ }
+
+ @override
+ Future deleteCredential(String accountId) {
+ return _delete(_credentialKey(accountId));
+ }
+
+ @override
+ Future clearActiveAccount() => _delete(activeAccountKey);
+
+ @override
+ Future migrateLegacyOAuthCredential(
+ String accountId,
+ BusyProvider provider,
+ ) async {
+ if (provider != BusyProvider.google && provider != BusyProvider.microsoft) {
+ return false;
+ }
+ if (await readCredential(accountId) != null) {
+ return false;
+ }
+ final accessToken = await _read(_legacyKey(accountId, 'access_token'));
+ final expiresAtText = await _read(_legacyKey(accountId, 'expires_at_utc'));
+ if (accessToken == null || expiresAtText == null) {
+ return false;
+ }
+ final tokenSet = OAuthTokenSet(
+ accessToken: accessToken,
+ refreshToken: await _read(_legacyKey(accountId, 'refresh_token')),
+ idToken: await _read(_legacyKey(accountId, 'id_token')),
+ expiresAtUtc: DateTime.parse(expiresAtText).toUtc(),
+ tokenType: await _read(_legacyKey(accountId, 'token_type')) ?? 'Bearer',
+ scopes: (await _read(_legacyKey(accountId, 'scope')) ?? '')
+ .split(RegExp(r'\s+'))
+ .where((scope) => scope.isNotEmpty)
+ .toSet(),
+ );
+ await saveOAuthTokenSet(accountId, provider, tokenSet);
+ final verified = await readOAuthTokenSet(accountId, provider);
+ if (verified == null || verified.accessToken != tokenSet.accessToken) {
+ throw const SecretStoreException(
+ 'SecretStoreMigrationVerificationFailed',
+ 'The OAuth credential migration could not be verified.',
+ );
+ }
+ await _deleteLegacyOAuthKeys(accountId);
+ return true;
+ }
+
+ String _credentialKey(String accountId) => 'busymax.secret.$accountId.v1';
+
+ String _legacyKey(String accountId, String name) =>
+ 'busymax.oauth.$accountId.$name';
+
+ Future _deleteLegacyOAuthKeys(String accountId) async {
+ for (final name in _legacyOAuthFieldNames) {
+ await _delete(_legacyKey(accountId, name));
+ }
+ }
+
+ Future _read(String key) async {
+ _logRuntime();
+ try {
+ return await _storage.read(key: key);
+ } on PlatformException catch (error) {
+ throw _secureStorageException('read', error);
+ }
+ }
+
+ Future _write(String key, String? value) async {
+ _logRuntime();
+ try {
+ await _storage.write(key: key, value: value);
+ } on PlatformException catch (error) {
+ throw _secureStorageException('write', error);
+ }
+ }
+
+ Future _delete(String key) async {
+ _logRuntime();
+ try {
+ await _storage.delete(key: key);
+ } on PlatformException catch (error) {
+ throw _secureStorageException('delete', error);
+ }
+ }
+
+ void _logRuntime() {
+ if (_loggedRuntime) {
+ return;
+ }
+ _loggedRuntime = true;
+ _logger.info(
+ 'Secret storage runtime: backend=flutter-secure-storage '
+ 'snap=${_isRunningInSnap()} secret_backend=${_secretBackendLabel()}',
+ );
+ }
+
+ SecretStoreException _secureStorageException(
+ String operation,
+ PlatformException error,
+ ) {
+ _logger.warning(
+ 'Secret storage $operation failed: '
+ '${sanitizedFlutterSecureStorageError(error)}',
+ );
+ return const SecretStoreException(
+ 'SecretStoreUnavailable',
+ secretStorageUnavailableMessage,
+ );
+ }
+}
+
+class InMemorySecretStore implements SecretStore {
+ final _credentials = {};
+ String? _activeAccountId;
+
+ @override
+ Future clearActiveAccount() async => _activeAccountId = null;
+
+ @override
+ Future deleteCredential(String accountId) async {
+ _credentials.remove(accountId);
+ }
+
+ @override
+ Future migrateLegacyOAuthCredential(
+ String accountId,
+ BusyProvider provider,
+ ) async => false;
+
+ @override
+ Future readActiveAccountId() async => _activeAccountId;
+
+ @override
+ Future readCredential(String accountId) async =>
+ _credentials[accountId];
+
+ @override
+ Future saveCredential(String accountId, SecretRecord credential) async {
+ _credentials[accountId] = credential;
+ }
+
+ @override
+ Future setActiveAccountId(String accountId) async {
+ _activeAccountId = accountId;
+ }
+}
+
+class SecretStoreException implements Exception {
+ const SecretStoreException(this.code, this.message);
+
+ final String code;
+ final String message;
+
+ @override
+ String toString() => '$code: $message';
+}
+
+class SecretStoreCorruptException extends SecretStoreException {
+ const SecretStoreCorruptException(String message)
+ : super('SecretStoreCorrupt', message);
+}
+
+class SecretStoreCredentialMismatchException extends SecretStoreException {
+ SecretStoreCredentialMismatchException({
+ required this.accountId,
+ required this.expectedProvider,
+ required this.actualProvider,
+ required this.actualKind,
+ }) : super(
+ 'SecretStoreCredentialMismatch',
+ 'The stored credential does not match the requested account provider.',
+ );
+
+ final String accountId;
+ final BusyProvider expectedProvider;
+ final BusyProvider actualProvider;
+ final CredentialKind actualKind;
+}
+
+const secretStorageUnavailableMessage =
+ 'Secure credential storage is locked. Unlock your system keyring and try again.';
+
+String sanitizedFlutterSecureStorageError(PlatformException error) {
+ final message = redactForLog(error.message).replaceAll(RegExp(r'\s+'), ' ');
+ return 'domain=flutter_secure_storage_linux code=${error.code} '
+ 'message=${message.trim()} details_type=${error.details.runtimeType}';
+}
+
+SecretRecord _decodeCredential(String serialized) {
+ try {
+ final decoded = jsonDecode(serialized);
+ if (decoded is! Map) {
+ throw const SecretStoreCorruptException(
+ 'The credential record is not a JSON object.',
+ );
+ }
+ return SecretRecord.fromJson(decoded.cast());
+ } on SecretStoreException {
+ rethrow;
+ } on Object catch (error) {
+ throw SecretStoreCorruptException(
+ 'The credential record could not be decoded (${error.runtimeType}).',
+ );
+ }
+}
+
+String _requiredSecretString(Map json, String key) {
+ final value = json[key]?.toString();
+ if (value == null || value.isEmpty) {
+ throw SecretStoreCorruptException('Credential record is missing $key.');
+ }
+ return value;
+}
+
+String? _optionalSecretString(Map json, String key) {
+ final value = json[key]?.toString();
+ return value == null || value.isEmpty ? null : value;
+}
+
+List _stringList(Object? value) {
+ if (value is! List) {
+ return const [];
+ }
+ return value.map((entry) => entry.toString()).toList(growable: false);
+}
+
+const _legacyOAuthFieldNames = [
+ 'access_token',
+ 'refresh_token',
+ 'id_token',
+ 'expires_at_utc',
+ 'token_type',
+ 'scope',
+];
+
+bool _isRunningInSnap() => Platform.environment['SNAP']?.isNotEmpty ?? false;
+
+String _secretBackendLabel() {
+ final backend = Platform.environment['SECRET_BACKEND'];
+ if (backend == null || backend.isEmpty) {
+ return '';
+ }
+ return backend == 'file' ? 'file' : '';
+}
diff --git a/lib/src/dav/auth/dav_account_dialogs.dart b/lib/src/dav/auth/dav_account_dialogs.dart
new file mode 100644
index 0000000..34f04c3
--- /dev/null
+++ b/lib/src/dav/auth/dav_account_dialogs.dart
@@ -0,0 +1,230 @@
+import 'package:flutter/material.dart';
+
+import '../../app/busymax_design.dart';
+import '../../app/busymax_dialogs.dart';
+import '../../l10n/l10n.dart';
+import '../../platform/linux_header_bar_service.dart';
+
+final class AppleICloudCredentialInput {
+ AppleICloudCredentialInput({required this.email, required this.password});
+
+ final String email;
+ final String password;
+
+ @override
+ String toString() =>
+ 'AppleICloudCredentialInput(email: [REDACTED], password: [REDACTED])';
+}
+
+Future showAppleICloudCredentialDialog(
+ BuildContext context, {
+ String? fixedEmail,
+ LinuxHeaderBarService? headerBarService,
+}) {
+ return showBusyMaxModalDialog(
+ context,
+ headerBarService: headerBarService,
+ barrierDismissible: false,
+ builder: (context) => _AppleCredentialDialog(fixedEmail: fixedEmail),
+ );
+}
+
+Future showNextcloudServerDialog(
+ BuildContext context, {
+ String? initialServer,
+ LinuxHeaderBarService? headerBarService,
+}) {
+ return showBusyMaxModalDialog(
+ context,
+ headerBarService: headerBarService,
+ barrierDismissible: false,
+ builder: (context) => _NextcloudServerDialog(initialServer: initialServer),
+ );
+}
+
+final class _AppleCredentialDialog extends StatefulWidget {
+ const _AppleCredentialDialog({required this.fixedEmail});
+
+ final String? fixedEmail;
+
+ @override
+ State<_AppleCredentialDialog> createState() => _AppleCredentialDialogState();
+}
+
+final class _AppleCredentialDialogState extends State<_AppleCredentialDialog> {
+ late final TextEditingController _email = TextEditingController(
+ text: widget.fixedEmail ?? '',
+ );
+ final TextEditingController _password = TextEditingController();
+ final FocusNode _emailFocus = FocusNode();
+ final FocusNode _passwordFocus = FocusNode();
+ var _submitted = false;
+
+ @override
+ void dispose() {
+ _email.dispose();
+ _password.dispose();
+ _emailFocus.dispose();
+ _passwordFocus.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final l10n = context.l10n;
+ final emailMissing = _submitted && _email.text.trim().isEmpty;
+ final passwordMissing = _submitted && _password.text.trim().isEmpty;
+ return BusyMaxDialogShell(
+ title: l10n.connectAppleICloudTitle,
+ maxWidth: 520,
+ actions: [
+ BusyMaxPushButton.standard(
+ onPressed: () => Navigator.of(context).pop(),
+ child: Text(l10n.cancel),
+ ),
+ BusyMaxPushButton.suggested(
+ onPressed: _submit,
+ child: Text(l10n.connectAccountAction),
+ ),
+ ],
+ children: [
+ Text(l10n.appleAppSpecificPasswordHelp),
+ const SizedBox(height: BusyMaxSpacing.md),
+ TextField(
+ key: const Key('apple-account-email-field'),
+ controller: _email,
+ focusNode: _emailFocus,
+ autofocus: widget.fixedEmail == null,
+ readOnly: widget.fixedEmail != null,
+ keyboardType: TextInputType.emailAddress,
+ autofillHints: const [AutofillHints.username, AutofillHints.email],
+ textInputAction: TextInputAction.next,
+ onSubmitted: (_) => _passwordFocus.requestFocus(),
+ onChanged: (_) => setState(() {}),
+ decoration: InputDecoration(
+ labelText: l10n.appleAccountEmail,
+ errorText: emailMissing ? l10n.requiredField : null,
+ ),
+ ),
+ const SizedBox(height: BusyMaxSpacing.md),
+ Semantics(
+ textField: true,
+ label: l10n.appleAppSpecificPassword,
+ child: TextField(
+ key: const Key('apple-app-specific-password-field'),
+ controller: _password,
+ focusNode: _passwordFocus,
+ autofocus: widget.fixedEmail != null,
+ obscureText: true,
+ enableSuggestions: false,
+ autocorrect: false,
+ autofillHints: const [AutofillHints.password],
+ textInputAction: TextInputAction.done,
+ onSubmitted: (_) => _submit(),
+ onChanged: (_) => setState(() {}),
+ decoration: InputDecoration(
+ labelText: l10n.appleAppSpecificPassword,
+ errorText: passwordMissing ? l10n.requiredField : null,
+ ),
+ ),
+ ),
+ const SizedBox(height: BusyMaxSpacing.md),
+ Text(
+ l10n.appleAppSpecificPasswordResetWarning,
+ style: Theme.of(context).textTheme.bodySmall,
+ ),
+ const SizedBox(height: BusyMaxSpacing.sm),
+ Text(
+ l10n.davCachedOfflineNotice,
+ style: Theme.of(context).textTheme.bodySmall,
+ ),
+ ],
+ );
+ }
+
+ void _submit() {
+ setState(() => _submitted = true);
+ final email = _email.text.trim();
+ final password = _password.text.trim();
+ if (email.isEmpty || password.isEmpty) return;
+ Navigator.of(
+ context,
+ ).pop(AppleICloudCredentialInput(email: email, password: password));
+ }
+}
+
+final class _NextcloudServerDialog extends StatefulWidget {
+ const _NextcloudServerDialog({required this.initialServer});
+
+ final String? initialServer;
+
+ @override
+ State<_NextcloudServerDialog> createState() => _NextcloudServerDialogState();
+}
+
+final class _NextcloudServerDialogState extends State<_NextcloudServerDialog> {
+ late final TextEditingController _server = TextEditingController(
+ text: widget.initialServer ?? '',
+ );
+ var _submitted = false;
+
+ @override
+ void dispose() {
+ _server.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final l10n = context.l10n;
+ return BusyMaxDialogShell(
+ title: l10n.connectNextcloudTitle,
+ maxWidth: 520,
+ actions: [
+ BusyMaxPushButton.standard(
+ onPressed: () => Navigator.of(context).pop(),
+ child: Text(l10n.cancel),
+ ),
+ BusyMaxPushButton.suggested(
+ onPressed: _submit,
+ child: Text(l10n.connectAccountAction),
+ ),
+ ],
+ children: [
+ TextField(
+ key: const Key('nextcloud-server-field'),
+ controller: _server,
+ autofocus: true,
+ keyboardType: TextInputType.url,
+ autofillHints: const [AutofillHints.url],
+ textInputAction: TextInputAction.done,
+ onChanged: (_) => setState(() {}),
+ onSubmitted: (_) => _submit(),
+ decoration: InputDecoration(
+ labelText: l10n.nextcloudServerUrl,
+ hintText: 'https://cloud.example.com/remote.php/dav',
+ helperText: l10n.nextcloudServerUrlHelp,
+ helperMaxLines: 2,
+ errorText: _submitted && _server.text.trim().isEmpty
+ ? l10n.requiredField
+ : null,
+ ),
+ ),
+ const SizedBox(height: BusyMaxSpacing.md),
+ Text(l10n.nextcloudBrowserAuthorizationHelp),
+ const SizedBox(height: BusyMaxSpacing.sm),
+ Text(
+ l10n.davCachedOfflineNotice,
+ style: Theme.of(context).textTheme.bodySmall,
+ ),
+ ],
+ );
+ }
+
+ void _submit() {
+ setState(() => _submitted = true);
+ final server = _server.text.trim();
+ if (server.isEmpty) return;
+ Navigator.of(context).pop(server);
+ }
+}
diff --git a/lib/src/dav/auth/dav_account_onboarding_service.dart b/lib/src/dav/auth/dav_account_onboarding_service.dart
new file mode 100644
index 0000000..1f01ae4
--- /dev/null
+++ b/lib/src/dav/auth/dav_account_onboarding_service.dart
@@ -0,0 +1,468 @@
+import 'package:drift/drift.dart';
+import 'package:uuid/uuid.dart';
+
+import '../../core/secrets/secret_store.dart';
+import '../../db/app_database.dart';
+import '../../features/accounts/data/accounts_repository.dart';
+import '../../providers/account_authority.dart';
+import '../../providers/busy_provider.dart';
+import '../dav_errors.dart';
+import '../discovery/dav_discovery_models.dart';
+import '../discovery/dav_discovery_repository.dart';
+import '../http/dav_http_transport.dart';
+import 'nextcloud_login_flow_v2.dart';
+
+typedef DavOnboardingDiscovery =
+ Future Function({
+ required String accountId,
+ required BusyProvider provider,
+ required Uri accountAuthority,
+ required DavBasicCredential credential,
+ DavCancellationToken? cancellationToken,
+ });
+
+typedef NextcloudCredentialRevoker =
+ Future Function({
+ required String accountId,
+ required NextcloudSecretRecord credential,
+ });
+
+final class DavAccountConnectionResult {
+ const DavAccountConnectionResult({
+ required this.accountId,
+ required this.discovery,
+ });
+
+ final String accountId;
+ final DavDiscoveryResult discovery;
+}
+
+final class DavAccountRemovalResult {
+ const DavAccountRemovalResult({
+ required this.remoteRevocationAttempted,
+ required this.remoteRevocationSucceeded,
+ required this.remoteFailureCode,
+ });
+
+ final bool remoteRevocationAttempted;
+ final bool remoteRevocationSucceeded;
+ final String? remoteFailureCode;
+}
+
+final class DavAccountOnboardingService {
+ DavAccountOnboardingService({
+ required AppDatabase database,
+ required SecretStore secretStore,
+ required DavOnboardingDiscovery discover,
+ required NextcloudLoginFlowV2 nextcloudLoginFlow,
+ AccountsRepository? accountsRepository,
+ DavDiscoveryRepository? discoveryRepository,
+ NextcloudCredentialRevoker? nextcloudCredentialRevoker,
+ String Function()? idFactory,
+ DateTime Function()? nowUtc,
+ }) : _database = database,
+ _secretStore = secretStore,
+ _discover = discover,
+ _nextcloudLoginFlow = nextcloudLoginFlow,
+ _accountsRepository =
+ accountsRepository ?? AccountsRepository(database: database),
+ _discoveryRepository =
+ discoveryRepository ?? DavDiscoveryRepository(database: database),
+ _nextcloudCredentialRevoker = nextcloudCredentialRevoker,
+ _idFactory = idFactory ?? const Uuid().v4,
+ _nowUtc = nowUtc ?? (() => DateTime.now().toUtc());
+
+ final AppDatabase _database;
+ final SecretStore _secretStore;
+ final DavOnboardingDiscovery _discover;
+ final NextcloudLoginFlowV2 _nextcloudLoginFlow;
+ final AccountsRepository _accountsRepository;
+ final DavDiscoveryRepository _discoveryRepository;
+ final NextcloudCredentialRevoker? _nextcloudCredentialRevoker;
+ final String Function() _idFactory;
+ final DateTime Function() _nowUtc;
+
+ Future connectAppleICloud({
+ required String email,
+ required String appSpecificPassword,
+ DavCancellationToken? cancellationToken,
+ }) {
+ final credential = AppleICloudSecretRecord(
+ username: email,
+ appSpecificPassword: appSpecificPassword,
+ );
+ return _connect(
+ provider: BusyProvider.appleICloud,
+ authority: Uri.parse(appleICloudAccountAuthority),
+ providerAccountId: credential.username,
+ credential: credential,
+ basicCredential: DavBasicCredential(
+ username: credential.username,
+ password: credential.appSpecificPassword,
+ ),
+ calendarsEnabled: true,
+ tasksEnabled: false,
+ email: credential.username,
+ displayName: credential.username,
+ cancellationToken: cancellationToken,
+ );
+ }
+
+ Future connectNextcloud({
+ required String enteredServer,
+ DavCancellationToken? cancellationToken,
+ }) async {
+ final login = await _nextcloudLoginFlow.start(enteredServer);
+ cancellationToken?.throwIfCancelled();
+ return _connectNextcloudLogin(login, cancellationToken: cancellationToken);
+ }
+
+ Future reconnectNextcloud({
+ required String accountId,
+ required String enteredServer,
+ DavCancellationToken? cancellationToken,
+ }) async {
+ final account = await _accountsRepository.accountById(accountId);
+ if (account == null || account.provider != BusyProvider.nextcloud) {
+ throw const DavException(
+ kind: DavErrorKind.authentication,
+ code: 'DavNextcloudAccountUnavailable',
+ safeMessage: 'The Nextcloud account could not be reconnected.',
+ );
+ }
+ final login = await _nextcloudLoginFlow.start(enteredServer);
+ cancellationToken?.throwIfCancelled();
+ final authority = normalizeAccountAuthority(
+ BusyProvider.nextcloud,
+ authority: login.canonicalServer.toString(),
+ );
+ final providerId = normalizeProviderAccountId(
+ BusyProvider.nextcloud,
+ login.loginName,
+ );
+ if (authority != account.authority ||
+ providerId != account.providerAccountId) {
+ throw const DavException(
+ kind: DavErrorKind.authentication,
+ code: 'DavNextcloudReconnectIdentityMismatch',
+ safeMessage: 'Nextcloud returned a different account during reconnect.',
+ );
+ }
+ final result = await _connectNextcloudLogin(
+ login,
+ cancellationToken: cancellationToken,
+ );
+ if (result.accountId != accountId) {
+ throw const DavException(
+ kind: DavErrorKind.protocol,
+ code: 'DavNextcloudReconnectAccountMismatch',
+ safeMessage: 'The Nextcloud account identity could not be restored.',
+ );
+ }
+ return result;
+ }
+
+ Future _connectNextcloudLogin(
+ NextcloudLoginFlowResult login, {
+ DavCancellationToken? cancellationToken,
+ }) {
+ final credential = NextcloudSecretRecord(
+ canonicalServer: login.canonicalServer,
+ loginName: login.loginName,
+ appPassword: login.appPassword,
+ );
+ return _connect(
+ provider: BusyProvider.nextcloud,
+ authority: credential.canonicalServer,
+ providerAccountId: credential.loginName,
+ credential: credential,
+ basicCredential: DavBasicCredential(
+ username: credential.loginName,
+ password: credential.appPassword,
+ ),
+ calendarsEnabled: true,
+ tasksEnabled: true,
+ displayName: credential.loginName,
+ cancellationToken: cancellationToken,
+ );
+ }
+
+ void cancelNextcloudLogin() => _nextcloudLoginFlow.cancel();
+
+ Future replaceAppleAppSpecificPassword({
+ required String accountId,
+ required String appSpecificPassword,
+ DavCancellationToken? cancellationToken,
+ }) async {
+ final account = await _accountsRepository.accountById(accountId);
+ final previous = await _secretStore.readCredential(accountId);
+ if (account == null ||
+ account.provider != BusyProvider.appleICloud ||
+ previous is! AppleICloudSecretRecord) {
+ throw const DavException(
+ kind: DavErrorKind.authentication,
+ code: 'DavAppleAccountUnavailable',
+ safeMessage: 'The Apple iCloud account could not be reconnected.',
+ );
+ }
+ final replacement = AppleICloudSecretRecord(
+ username: previous.username,
+ appSpecificPassword: appSpecificPassword,
+ );
+ final discovery = await _discover(
+ accountId: accountId,
+ provider: BusyProvider.appleICloud,
+ accountAuthority: Uri.parse(appleICloudAccountAuthority),
+ credential: DavBasicCredential(
+ username: replacement.username,
+ password: replacement.appSpecificPassword,
+ ),
+ cancellationToken: cancellationToken,
+ );
+ _validateDiscovery(
+ discovery,
+ accountId: accountId,
+ provider: BusyProvider.appleICloud,
+ );
+ await _replaceCredentialVerified(accountId, replacement, previous);
+ try {
+ await _database.transaction(() async {
+ await _accountsRepository.upsertSignedInAccount(
+ id: accountId,
+ provider: BusyProvider.appleICloud,
+ providerAccountId: account.providerAccountId,
+ authority: appleICloudAccountAuthority,
+ credentialKind: CredentialKind.appleAppSpecificPassword,
+ displayName: account.displayName,
+ email: account.email,
+ grantedScopes: '',
+ calendarsEnabled: true,
+ tasksEnabled: false,
+ );
+ await _discoveryRepository.commitSuccessfulInventory(discovery);
+ await _requeueAuthenticationBlockedOperations(accountId);
+ });
+ return DavAccountConnectionResult(
+ accountId: accountId,
+ discovery: discovery,
+ );
+ } on Object {
+ await _secretStore.saveCredential(accountId, previous);
+ rethrow;
+ }
+ }
+
+ Future removeAccount(String accountId) async {
+ final account = await _accountsRepository.accountById(accountId);
+ if (account == null) {
+ return const DavAccountRemovalResult(
+ remoteRevocationAttempted: false,
+ remoteRevocationSucceeded: false,
+ remoteFailureCode: null,
+ );
+ }
+ final credential = await _secretStore.readCredential(accountId);
+ var attempted = false;
+ var succeeded = false;
+ String? failureCode;
+ if (account.provider == BusyProvider.nextcloud &&
+ credential is NextcloudSecretRecord &&
+ _nextcloudCredentialRevoker != null) {
+ attempted = true;
+ try {
+ await _nextcloudCredentialRevoker(
+ accountId: accountId,
+ credential: credential,
+ );
+ succeeded = true;
+ } on DavException catch (error) {
+ failureCode = error.code;
+ } on Object {
+ failureCode = 'NextcloudAppPasswordRevocationFailed';
+ }
+ }
+
+ // Local removal is deliberately independent of remote revocation success.
+ await _secretStore.deleteCredential(accountId);
+ if (await _secretStore.readActiveAccountId() == accountId) {
+ await _secretStore.clearActiveAccount();
+ }
+ await _accountsRepository.deleteAccount(accountId);
+ return DavAccountRemovalResult(
+ remoteRevocationAttempted: attempted,
+ remoteRevocationSucceeded: succeeded,
+ remoteFailureCode: failureCode,
+ );
+ }
+
+ Future _connect({
+ required BusyProvider provider,
+ required Uri authority,
+ required String providerAccountId,
+ required SecretRecord credential,
+ required DavBasicCredential basicCredential,
+ required bool calendarsEnabled,
+ required bool tasksEnabled,
+ required String displayName,
+ String? email,
+ DavCancellationToken? cancellationToken,
+ }) async {
+ final normalizedAuthority = normalizeAccountAuthority(
+ provider,
+ authority: authority.toString(),
+ );
+ final normalizedProviderAccountId = normalizeProviderAccountId(
+ provider,
+ providerAccountId,
+ );
+ final existing =
+ await (_database.select(_database.accounts)..where(
+ (row) =>
+ row.provider.equals(provider.storageValue) &
+ row.authority.equals(normalizedAuthority) &
+ row.providerAccountId.equals(normalizedProviderAccountId),
+ ))
+ .getSingleOrNull();
+ final accountId =
+ existing?.id ?? '${provider.storageValue}:${_idFactory()}';
+ final previousCredential = existing == null
+ ? null
+ : await _secretStore.readCredential(accountId);
+
+ // No durable credential or connected account state is written until the
+ // supplied credential has completed principal/home/collection discovery.
+ final discovery = await _discover(
+ accountId: accountId,
+ provider: provider,
+ accountAuthority: Uri.parse(normalizedAuthority),
+ credential: basicCredential,
+ cancellationToken: cancellationToken,
+ );
+ _validateDiscovery(discovery, accountId: accountId, provider: provider);
+ await _replaceCredentialVerified(accountId, credential, previousCredential);
+ try {
+ await _database.transaction(() async {
+ await _accountsRepository.upsertSignedInAccount(
+ id: accountId,
+ provider: provider,
+ providerAccountId: normalizedProviderAccountId,
+ authority: normalizedAuthority,
+ credentialKind: credential.kind,
+ displayName: displayName,
+ email: email,
+ grantedScopes: '',
+ calendarsEnabled: calendarsEnabled,
+ tasksEnabled: tasksEnabled,
+ );
+ await _discoveryRepository.commitSuccessfulInventory(discovery);
+ await _requeueAuthenticationBlockedOperations(accountId);
+ });
+ await _secretStore.setActiveAccountId(accountId);
+ return DavAccountConnectionResult(
+ accountId: accountId,
+ discovery: discovery,
+ );
+ } on Object {
+ if (previousCredential != null) {
+ await _secretStore.saveCredential(accountId, previousCredential);
+ } else {
+ await _secretStore.deleteCredential(accountId);
+ await _accountsRepository.deleteAccount(accountId);
+ }
+ rethrow;
+ }
+ }
+
+ Future _replaceCredentialVerified(
+ String accountId,
+ SecretRecord replacement,
+ SecretRecord? previous,
+ ) async {
+ await _secretStore.saveCredential(accountId, replacement);
+ try {
+ final readBack = await _secretStore.readCredential(accountId);
+ if (!_sameCredential(readBack, replacement)) {
+ throw const SecretStoreException(
+ 'SecretStoreWriteVerificationFailed',
+ 'The credential could not be verified after secure storage.',
+ );
+ }
+ } on Object {
+ if (previous != null) {
+ await _secretStore.saveCredential(accountId, previous);
+ } else {
+ await _secretStore.deleteCredential(accountId);
+ }
+ rethrow;
+ }
+ }
+
+ Future _requeueAuthenticationBlockedOperations(String accountId) async {
+ await (_database.update(_database.pendingOps)..where(
+ (row) =>
+ row.accountId.equals(accountId) &
+ row.operationType.like('dav.%') &
+ row.state.equals('auth_blocked'),
+ ))
+ .write(
+ PendingOpsCompanion(
+ state: const Value('pending'),
+ retryClassification: const Value('credential_replaced'),
+ attemptCount: const Value(0),
+ nextAttemptAtUtc: const Value(null),
+ lastErrorCode: const Value(null),
+ lastErrorMessage: const Value(null),
+ lastError: const Value(null),
+ updatedAtUtc: Value(_nowUtc().toUtc().toIso8601String()),
+ ),
+ );
+ }
+}
+
+void _validateDiscovery(
+ DavDiscoveryResult discovery, {
+ required String accountId,
+ required BusyProvider provider,
+}) {
+ if (discovery.accountId != accountId ||
+ discovery.provider != provider ||
+ !discovery.service.capabilities.hasPrincipal ||
+ !discovery.service.capabilities.hasCalendarHome) {
+ throw const DavException(
+ kind: DavErrorKind.protocol,
+ code: 'DavDiscoveryIdentityMismatch',
+ safeMessage: 'DAV discovery returned an invalid account identity.',
+ );
+ }
+}
+
+bool _sameCredential(SecretRecord? left, SecretRecord right) =>
+ switch ((left, right)) {
+ (
+ AppleICloudSecretRecord(
+ username: final leftUser,
+ appSpecificPassword: final leftPassword,
+ ),
+ AppleICloudSecretRecord(
+ username: final rightUser,
+ appSpecificPassword: final rightPassword,
+ ),
+ ) =>
+ leftUser == rightUser && leftPassword == rightPassword,
+ (
+ NextcloudSecretRecord(
+ canonicalServer: final leftServer,
+ loginName: final leftUser,
+ appPassword: final leftPassword,
+ ),
+ NextcloudSecretRecord(
+ canonicalServer: final rightServer,
+ loginName: final rightUser,
+ appPassword: final rightPassword,
+ ),
+ ) =>
+ leftServer == rightServer &&
+ leftUser == rightUser &&
+ leftPassword == rightPassword,
+ _ => false,
+ };
diff --git a/lib/src/dav/auth/nextcloud_app_password_revoker.dart b/lib/src/dav/auth/nextcloud_app_password_revoker.dart
new file mode 100644
index 0000000..8c0d51f
--- /dev/null
+++ b/lib/src/dav/auth/nextcloud_app_password_revoker.dart
@@ -0,0 +1,58 @@
+import '../../core/secrets/secret_store.dart';
+import '../dav_errors.dart';
+import '../http/dav_http_transport.dart';
+
+final class NextcloudAppPasswordRevoker {
+ const NextcloudAppPasswordRevoker({required DavHttpTransport transport})
+ : _transport = transport;
+
+ final DavHttpTransport _transport;
+
+ Future revoke({
+ required String accountId,
+ required NextcloudSecretRecord credential,
+ required String correlationId,
+ }) async {
+ final endpoint = _appendPath(
+ credential.canonicalServer,
+ 'ocs/v2.php/core/apppassword',
+ );
+ final response = await _transport.send(
+ DavRequest(
+ method: 'DELETE',
+ uri: endpoint,
+ accountId: accountId,
+ correlationId: correlationId,
+ headers: const {'ocs-apirequest': 'true', 'accept': 'application/json'},
+ retryClass: DavRetryClass.never,
+ ),
+ credential: DavBasicCredential(
+ username: credential.loginName,
+ password: credential.appPassword,
+ ),
+ );
+ if (response.statusCode < 200 || response.statusCode >= 300) {
+ throw DavException(
+ kind: response.statusCode == 401
+ ? DavErrorKind.authentication
+ : response.statusCode == 403
+ ? DavErrorKind.authorization
+ : response.statusCode >= 500
+ ? DavErrorKind.server
+ : DavErrorKind.protocol,
+ code: 'NextcloudAppPasswordRevocationFailed',
+ safeMessage: 'The Nextcloud app password could not be revoked.',
+ statusCode: response.statusCode,
+ correlationId: correlationId,
+ retryAfter: parseDavRetryAfter(response.headers['retry-after']),
+ );
+ }
+ }
+}
+
+Uri _appendPath(Uri base, String suffix) {
+ final path = base.path.isEmpty
+ ? '/$suffix'
+ : '${base.path.endsWith('/') ? base.path : '${base.path}/'}$suffix';
+ return base.replace(path: path, query: null, fragment: null);
+}
diff --git a/lib/src/dav/auth/nextcloud_login_flow_v2.dart b/lib/src/dav/auth/nextcloud_login_flow_v2.dart
new file mode 100644
index 0000000..234ce64
--- /dev/null
+++ b/lib/src/dav/auth/nextcloud_login_flow_v2.dart
@@ -0,0 +1,460 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:http/http.dart' as http;
+import 'package:url_launcher/url_launcher.dart';
+
+import '../../providers/account_authority.dart';
+import '../dav_errors.dart';
+import '../http/dav_http_transport.dart';
+
+typedef NextcloudBrowserLauncher = Future Function(Uri loginUri);
+typedef NextcloudLoginDelay = Future Function(Duration duration);
+
+final class NextcloudLoginFlowResult {
+ NextcloudLoginFlowResult({
+ required this.canonicalServer,
+ required String loginName,
+ required String appPassword,
+ }) : loginName = loginName.trim(),
+ appPassword = appPassword.trim() {
+ if (this.loginName.isEmpty || this.appPassword.isEmpty) {
+ throw const DavException(
+ kind: DavErrorKind.authentication,
+ code: 'NextcloudLoginFlowMalformedCredential',
+ safeMessage: 'Nextcloud returned an incomplete app credential.',
+ );
+ }
+ }
+
+ final Uri canonicalServer;
+ final String loginName;
+ final String appPassword;
+
+ @override
+ String toString() => 'NextcloudLoginFlowResult(credentials: [REDACTED])';
+}
+
+final class NextcloudLoginFlowV2 {
+ NextcloudLoginFlowV2({
+ required http.Client client,
+ NextcloudBrowserLauncher? browserLauncher,
+ NextcloudLoginDelay? delay,
+ DateTime Function()? nowUtc,
+ this.pollInterval = const Duration(seconds: 1),
+ this.operationTimeout = const Duration(minutes: 10),
+ this.responseTimeout = const Duration(seconds: 30),
+ this.maximumResponseBytes = 64 * 1024,
+ this.maximumRedirects = 3,
+ }) : _client = client,
+ _browserLauncher = browserLauncher ?? _launchExternalBrowser,
+ _delay = delay ?? Future.delayed,
+ _nowUtc = nowUtc ?? (() => DateTime.now().toUtc()) {
+ if (pollInterval <= Duration.zero ||
+ operationTimeout <= Duration.zero ||
+ responseTimeout <= Duration.zero ||
+ maximumResponseBytes < 1 ||
+ maximumRedirects < 0) {
+ throw ArgumentError('Nextcloud Login Flow limits must be positive.');
+ }
+ }
+
+ final http.Client _client;
+ final NextcloudBrowserLauncher _browserLauncher;
+ final NextcloudLoginDelay _delay;
+ final DateTime Function() _nowUtc;
+ final Duration pollInterval;
+ final Duration operationTimeout;
+ final Duration responseTimeout;
+ final int maximumResponseBytes;
+ final int maximumRedirects;
+
+ Future? _activeOperation;
+ DavCancellationToken? _activeCancellation;
+
+ Future start(String enteredServer) {
+ if (_activeOperation != null) {
+ throw const DavException(
+ kind: DavErrorKind.conflict,
+ code: 'NextcloudLoginFlowAlreadyRunning',
+ safeMessage: 'A Nextcloud connection is already in progress.',
+ );
+ }
+ final cancellation = DavCancellationToken();
+ _activeCancellation = cancellation;
+ final operation = _run(enteredServer, cancellation);
+ _activeOperation = operation;
+ unawaited(
+ operation.then(
+ (_) => _clear(operation),
+ onError: (_, _) => _clear(operation),
+ ),
+ );
+ return operation;
+ }
+
+ void cancel() => _activeCancellation?.cancel();
+
+ Future _run(
+ String enteredServer,
+ DavCancellationToken cancellation,
+ ) async {
+ final baseServer = normalizeNextcloudLoginServer(enteredServer);
+ final startUri = _appendPath(baseServer, 'index.php/login/v2');
+ final deadline = _nowUtc().toUtc().add(operationTimeout);
+ cancellation.throwIfCancelled();
+ final startResponse = await _postFollowingRedirects(
+ startUri,
+ body: null,
+ trustedBase: baseServer,
+ cancellation: cancellation,
+ );
+ if (startResponse.statusCode != HttpStatus.ok) {
+ throw DavException(
+ kind: startResponse.statusCode >= 500
+ ? DavErrorKind.server
+ : DavErrorKind.authentication,
+ code: 'NextcloudLoginFlowStartRejected',
+ safeMessage: 'Nextcloud could not start browser authorization.',
+ statusCode: startResponse.statusCode,
+ );
+ }
+ final startJson = _jsonObject(startResponse.bodyBytes);
+ final pollJson = _jsonObjectValue(startJson, 'poll');
+ final token = _requiredJsonString(pollJson, 'token');
+ final pollEndpoint = _validatedFlowUri(
+ _requiredJsonString(pollJson, 'endpoint'),
+ responseUri: startResponse.requestUri,
+ installationPath: baseServer.path,
+ allowQuery: false,
+ );
+ final loginUri = _validatedFlowUri(
+ _requiredJsonString(startJson, 'login'),
+ responseUri: startResponse.requestUri,
+ installationPath: baseServer.path,
+ allowQuery: true,
+ );
+ cancellation.throwIfCancelled();
+ final launched = await _browserLauncher(loginUri);
+ if (!launched) {
+ throw const DavException(
+ kind: DavErrorKind.protocol,
+ code: 'NextcloudLoginBrowserLaunchFailed',
+ safeMessage: 'The Nextcloud sign-in page could not be opened.',
+ );
+ }
+
+ while (true) {
+ cancellation.throwIfCancelled();
+ if (!_nowUtc().toUtc().isBefore(deadline)) {
+ throw const DavException(
+ kind: DavErrorKind.timeout,
+ code: 'NextcloudLoginFlowExpired',
+ safeMessage: 'The Nextcloud sign-in request expired.',
+ );
+ }
+ final pollResponse = await _postFollowingRedirects(
+ pollEndpoint,
+ body: {'token': token},
+ trustedBase: pollEndpoint.replace(path: baseServer.path),
+ cancellation: cancellation,
+ );
+ if (pollResponse.statusCode == HttpStatus.notFound) {
+ await _delay(pollInterval);
+ continue;
+ }
+ if (pollResponse.statusCode != HttpStatus.ok) {
+ throw DavException(
+ kind: pollResponse.statusCode >= 500
+ ? DavErrorKind.server
+ : DavErrorKind.authentication,
+ code: 'NextcloudLoginFlowRejected',
+ safeMessage: 'Nextcloud did not complete browser authorization.',
+ statusCode: pollResponse.statusCode,
+ );
+ }
+ final completed = _jsonObject(pollResponse.bodyBytes);
+ final normalizedAuthority = normalizeNextcloudServerAuthority(
+ _requiredJsonString(completed, 'server'),
+ );
+ return NextcloudLoginFlowResult(
+ canonicalServer: Uri.parse(normalizedAuthority),
+ loginName: _requiredJsonString(completed, 'loginName'),
+ appPassword: _requiredJsonString(completed, 'appPassword'),
+ );
+ }
+ }
+
+ Future<_FlowResponse> _postFollowingRedirects(
+ Uri initialUri, {
+ required Map? body,
+ required Uri trustedBase,
+ required DavCancellationToken cancellation,
+ }) async {
+ var current = initialUri;
+ final visited = {};
+ for (var redirects = 0; redirects <= maximumRedirects; redirects += 1) {
+ cancellation.throwIfCancelled();
+ if (!visited.add(current.toString())) {
+ throw _redirectError('NextcloudLoginRedirectLoop');
+ }
+ if (!_safeHttps(current) ||
+ !_sameOrigin(current, trustedBase) ||
+ !_withinInstallationPath(current.path, trustedBase.path)) {
+ throw _redirectError('NextcloudLoginRedirectRejected');
+ }
+ final request = http.Request('POST', current)
+ ..followRedirects = false
+ ..headers['accept'] = 'application/json';
+ if (body != null) {
+ request.headers['content-type'] =
+ 'application/x-www-form-urlencoded; charset=utf-8';
+ request.body = _formEncode(body);
+ }
+ http.StreamedResponse streamed;
+ try {
+ streamed = await _client.send(request).timeout(responseTimeout);
+ } on TimeoutException {
+ throw const DavException(
+ kind: DavErrorKind.timeout,
+ code: 'NextcloudLoginResponseTimeout',
+ safeMessage: 'The Nextcloud login server did not respond in time.',
+ );
+ } on HandshakeException {
+ throw const DavException(
+ kind: DavErrorKind.tls,
+ code: 'DavTlsFailure',
+ safeMessage: 'The Nextcloud TLS connection could not be verified.',
+ );
+ } on Object {
+ throw const DavException(
+ kind: DavErrorKind.network,
+ code: 'DavTransientNetwork',
+ safeMessage: 'The Nextcloud login server could not be reached.',
+ );
+ }
+ final bytes = await _readBody(streamed, cancellation);
+ if (!_isRedirect(streamed.statusCode)) {
+ return _FlowResponse(
+ statusCode: streamed.statusCode,
+ bodyBytes: bytes,
+ requestUri: current,
+ );
+ }
+ final location = streamed.headers['location'];
+ if (location == null || redirects == maximumRedirects) {
+ throw _redirectError('NextcloudLoginRedirectLimitExceeded');
+ }
+ final destination = current.resolve(location);
+ if (!_safeHttps(destination) ||
+ !_withinInstallationPath(destination.path, trustedBase.path) ||
+ !_sameOrigin(destination, trustedBase)) {
+ throw _redirectError('NextcloudLoginRedirectRejected');
+ }
+ current = destination;
+ }
+ throw StateError('Unreachable Nextcloud redirect state.');
+ }
+
+ Future _readBody(
+ http.StreamedResponse response,
+ DavCancellationToken cancellation,
+ ) async {
+ final bytes = BytesBuilder(copy: false);
+ var length = 0;
+ try {
+ await for (final chunk in response.stream.timeout(responseTimeout)) {
+ cancellation.throwIfCancelled();
+ length += chunk.length;
+ if (length > maximumResponseBytes) {
+ throw const DavException(
+ kind: DavErrorKind.responseTooLarge,
+ code: 'NextcloudLoginResponseTooLarge',
+ safeMessage: 'The Nextcloud login response was too large.',
+ );
+ }
+ bytes.add(chunk);
+ }
+ return bytes.takeBytes();
+ } on TimeoutException {
+ throw const DavException(
+ kind: DavErrorKind.timeout,
+ code: 'NextcloudLoginResponseTimeout',
+ safeMessage: 'The Nextcloud login server did not respond in time.',
+ );
+ }
+ }
+
+ void _clear(Future operation) {
+ if (identical(_activeOperation, operation)) {
+ _activeOperation = null;
+ _activeCancellation = null;
+ }
+ }
+}
+
+Uri normalizeNextcloudLoginServer(String enteredServer) {
+ var source = enteredServer.trim();
+ if (source.isEmpty) {
+ throw const FormatException('Enter a Nextcloud server address.');
+ }
+ if (!source.contains('://')) source = 'https://$source';
+ final parsed = Uri.tryParse(source);
+ if (parsed == null ||
+ (parsed.scheme != 'http' && parsed.scheme != 'https') ||
+ parsed.host.isEmpty ||
+ parsed.userInfo.isNotEmpty ||
+ parsed.hasQuery ||
+ parsed.hasFragment) {
+ throw const FormatException('Enter a valid Nextcloud HTTPS server.');
+ }
+ var path = parsed.normalizePath().path;
+ while (path.length > 1 && path.endsWith('/')) {
+ path = path.substring(0, path.length - 1);
+ }
+ path = _nextcloudInstallationPath(path);
+ return Uri(
+ scheme: 'https',
+ host: parsed.host.toLowerCase(),
+ port: parsed.hasPort && parsed.port != 443 ? parsed.port : null,
+ path: path == '/' ? '' : path,
+ );
+}
+
+String _nextcloudInstallationPath(String enteredPath) {
+ final normalized = enteredPath.toLowerCase();
+ const davEndpoints = [
+ '/remote.php/dav',
+ '/remote.php/caldav',
+ '/remote.php/webdav',
+ ];
+ for (final endpoint in davEndpoints) {
+ final index = normalized.indexOf(endpoint);
+ if (index < 0) continue;
+ final endpointEnd = index + endpoint.length;
+ if (endpointEnd == normalized.length ||
+ normalized.codeUnitAt(endpointEnd) == 0x2f) {
+ return enteredPath.substring(0, index);
+ }
+ }
+ return enteredPath;
+}
+
+final class _FlowResponse {
+ const _FlowResponse({
+ required this.statusCode,
+ required this.bodyBytes,
+ required this.requestUri,
+ });
+
+ final int statusCode;
+ final Uint8List bodyBytes;
+ final Uri requestUri;
+}
+
+Map _jsonObject(Uint8List bytes) {
+ try {
+ final decoded = jsonDecode(utf8.decode(bytes, allowMalformed: false));
+ if (decoded is! Map) throw const FormatException();
+ return decoded.cast();
+ } on Object {
+ throw const DavException(
+ kind: DavErrorKind.protocol,
+ code: 'NextcloudLoginMalformedResponse',
+ safeMessage: 'Nextcloud returned a malformed login response.',
+ );
+ }
+}
+
+Map _jsonObjectValue(Map json, String key) {
+ final value = json[key];
+ if (value is! Map) {
+ throw const DavException(
+ kind: DavErrorKind.protocol,
+ code: 'NextcloudLoginMalformedResponse',
+ safeMessage: 'Nextcloud returned a malformed login response.',
+ );
+ }
+ return value.cast();
+}
+
+String _requiredJsonString(Map json, String key) {
+ final value = json[key];
+ if (value is! String || value.trim().isEmpty || value.length > 16384) {
+ throw const DavException(
+ kind: DavErrorKind.protocol,
+ code: 'NextcloudLoginMalformedResponse',
+ safeMessage: 'Nextcloud returned a malformed login response.',
+ );
+ }
+ return value;
+}
+
+Uri _validatedFlowUri(
+ String source, {
+ required Uri responseUri,
+ required String installationPath,
+ required bool allowQuery,
+}) {
+ final uri = Uri.tryParse(source);
+ if (uri == null ||
+ !_safeHttps(uri) ||
+ !_sameOrigin(uri, responseUri) ||
+ !_withinInstallationPath(uri.path, installationPath) ||
+ (!allowQuery && uri.hasQuery)) {
+ throw _redirectError('NextcloudLoginEndpointRejected');
+ }
+ return uri;
+}
+
+Uri _appendPath(Uri base, String suffix) {
+ final path = base.path.isEmpty
+ ? '/$suffix'
+ : '${base.path.endsWith('/') ? base.path : '${base.path}/'}$suffix';
+ return base.replace(path: path);
+}
+
+String _formEncode(Map values) => values.entries
+ .map(
+ (entry) =>
+ '${Uri.encodeQueryComponent(entry.key)}='
+ '${Uri.encodeQueryComponent(entry.value)}',
+ )
+ .join('&');
+
+bool _safeHttps(Uri uri) =>
+ uri.scheme.toLowerCase() == 'https' &&
+ uri.host.isNotEmpty &&
+ uri.userInfo.isEmpty &&
+ !uri.hasFragment;
+
+bool _sameOrigin(Uri left, Uri right) =>
+ left.scheme.toLowerCase() == right.scheme.toLowerCase() &&
+ left.host.toLowerCase() == right.host.toLowerCase() &&
+ left.port == right.port;
+
+bool _withinInstallationPath(String candidate, String installationPath) {
+ var base = installationPath;
+ while (base.length > 1 && base.endsWith('/')) {
+ base = base.substring(0, base.length - 1);
+ }
+ if (base.isEmpty || base == '/') return candidate.startsWith('/');
+ return candidate == base || candidate.startsWith('$base/');
+}
+
+bool _isRedirect(int status) =>
+ status == HttpStatus.movedPermanently ||
+ status == HttpStatus.found ||
+ status == HttpStatus.temporaryRedirect ||
+ status == HttpStatus.permanentRedirect;
+
+DavException _redirectError(String code) => DavException(
+ kind: DavErrorKind.redirectRejected,
+ code: code,
+ safeMessage: 'Nextcloud returned an untrusted login destination.',
+);
+
+Future _launchExternalBrowser(Uri uri) =>
+ launchUrl(uri, mode: LaunchMode.externalApplication);
diff --git a/lib/src/dav/dav_errors.dart b/lib/src/dav/dav_errors.dart
new file mode 100644
index 0000000..cf75aa7
--- /dev/null
+++ b/lib/src/dav/dav_errors.dart
@@ -0,0 +1,293 @@
+import 'dart:io';
+
+enum DavErrorKind {
+ cancelled,
+ timeout,
+ network,
+ tls,
+ authentication,
+ authorization,
+ redirectRejected,
+ redirectLoop,
+ responseTooLarge,
+ malformedXml,
+ malformedStatus,
+ protocol,
+ rateLimited,
+ invalidSyncToken,
+ preconditionFailed,
+ uidConflict,
+ invalidCalendarData,
+ unsupportedComponent,
+ maximumResourceSize,
+ limitExceeded,
+ notFound,
+ conflict,
+ server,
+}
+
+/// DAV failure categories used by retry policy and user-facing error handling.
+enum DavErrorCategory {
+ davAuthRejected,
+ davCredentialsRevoked,
+ davTlsFailure,
+ davDiscoveryFailed,
+ davUnsupportedServer,
+ davPermissionDenied,
+ davResourceConflict,
+ davUidConflict,
+ davMalformedResource,
+ davUnsupportedComponent,
+ davQuotaOrSizeLimit,
+ davRateLimited,
+ davTransientNetwork,
+ davServerUnavailable,
+ davSyncTokenInvalid,
+ davCollectionRemoved,
+ davReadOnly,
+ davProtocolViolation,
+ davOperationCancelled,
+}
+
+final class DavErrorDisposition {
+ const DavErrorDisposition({
+ required this.category,
+ required this.retryable,
+ required this.requiresUserAction,
+ required this.cachedDataUsable,
+ });
+
+ final DavErrorCategory category;
+ final bool retryable;
+ final bool requiresUserAction;
+ final bool cachedDataUsable;
+}
+
+/// A typed DAV failure whose diagnostic representation excludes request URLs,
+/// response bodies, credentials, and user calendar content.
+final class DavException implements Exception {
+ const DavException({
+ required this.kind,
+ required this.code,
+ required this.safeMessage,
+ this.statusCode,
+ this.correlationId,
+ this.retryAfter,
+ this.categoryOverride,
+ });
+
+ final DavErrorKind kind;
+ final String code;
+ final String safeMessage;
+ final int? statusCode;
+ final String? correlationId;
+ final Duration? retryAfter;
+ final DavErrorCategory? categoryOverride;
+
+ DavErrorDisposition get disposition => classifyDavError(
+ kind: kind,
+ code: code,
+ statusCode: statusCode,
+ categoryOverride: categoryOverride,
+ );
+
+ DavErrorCategory get category => disposition.category;
+ bool get retryable => disposition.retryable;
+ bool get requiresUserAction => disposition.requiresUserAction;
+ bool get cachedDataUsable => disposition.cachedDataUsable;
+
+ @override
+ String toString() =>
+ 'DavException(kind: ${kind.name}, code: $code, '
+ 'statusCode: $statusCode, correlationId: $correlationId)';
+}
+
+DavErrorDisposition classifyDavError({
+ required DavErrorKind kind,
+ required String code,
+ int? statusCode,
+ DavErrorCategory? categoryOverride,
+}) {
+ final category =
+ categoryOverride ??
+ _categoryFor(kind: kind, code: code, statusCode: statusCode);
+ final retryable = switch (category) {
+ DavErrorCategory.davRateLimited ||
+ DavErrorCategory.davTransientNetwork ||
+ DavErrorCategory.davServerUnavailable => true,
+ DavErrorCategory.davDiscoveryFailed =>
+ kind == DavErrorKind.timeout ||
+ kind == DavErrorKind.network ||
+ kind == DavErrorKind.server ||
+ kind == DavErrorKind.rateLimited,
+ _ => false,
+ };
+ final requiresUserAction = switch (category) {
+ DavErrorCategory.davAuthRejected ||
+ DavErrorCategory.davCredentialsRevoked ||
+ DavErrorCategory.davTlsFailure ||
+ DavErrorCategory.davUnsupportedServer ||
+ DavErrorCategory.davPermissionDenied ||
+ DavErrorCategory.davResourceConflict ||
+ DavErrorCategory.davUidConflict ||
+ DavErrorCategory.davReadOnly => true,
+ DavErrorCategory.davDiscoveryFailed => !retryable,
+ _ => false,
+ };
+ final cachedDataUsable = switch (category) {
+ DavErrorCategory.davMalformedResource ||
+ DavErrorCategory.davUnsupportedComponent ||
+ DavErrorCategory.davCollectionRemoved => false,
+ _ => true,
+ };
+ return DavErrorDisposition(
+ category: category,
+ retryable: retryable,
+ requiresUserAction: requiresUserAction,
+ cachedDataUsable: cachedDataUsable,
+ );
+}
+
+DavErrorCategory _categoryFor({
+ required DavErrorKind kind,
+ required String code,
+ required int? statusCode,
+}) {
+ final normalizedCode = code.toLowerCase();
+ if (normalizedCode.contains('credentialsrevoked')) {
+ return DavErrorCategory.davCredentialsRevoked;
+ }
+ if (normalizedCode.contains('authrejected')) {
+ return DavErrorCategory.davAuthRejected;
+ }
+ if (normalizedCode.contains('unsupportedserver') ||
+ normalizedCode.contains('unsupportedprofile')) {
+ return DavErrorCategory.davUnsupportedServer;
+ }
+ if (normalizedCode.contains('discoveryfailed')) {
+ return DavErrorCategory.davDiscoveryFailed;
+ }
+ if (normalizedCode.contains('permissiondenied')) {
+ return DavErrorCategory.davPermissionDenied;
+ }
+ if (normalizedCode.contains('readonly')) {
+ return DavErrorCategory.davReadOnly;
+ }
+ if (normalizedCode.contains('uidconflict')) {
+ return DavErrorCategory.davUidConflict;
+ }
+ if (normalizedCode.contains('resourceconflict') ||
+ normalizedCode.contains('resourcelocked') ||
+ normalizedCode.startsWith('davconflict')) {
+ return DavErrorCategory.davResourceConflict;
+ }
+ if (normalizedCode.contains('malformedresource')) {
+ return DavErrorCategory.davMalformedResource;
+ }
+ if (normalizedCode.contains('unsupportedcomponent')) {
+ return DavErrorCategory.davUnsupportedComponent;
+ }
+ if (normalizedCode.contains('quotaorsizelimit') ||
+ normalizedCode.contains('maximumresourcesize')) {
+ return DavErrorCategory.davQuotaOrSizeLimit;
+ }
+ if (normalizedCode.contains('ratelimited')) {
+ return DavErrorCategory.davRateLimited;
+ }
+ if (normalizedCode.contains('tlsfailure')) {
+ return DavErrorCategory.davTlsFailure;
+ }
+ if (normalizedCode.contains('networkfailure') ||
+ normalizedCode.contains('timeout')) {
+ return DavErrorCategory.davTransientNetwork;
+ }
+ if (normalizedCode.contains('serverunavailable')) {
+ return DavErrorCategory.davServerUnavailable;
+ }
+ if (normalizedCode.contains('synctokeninvalid')) {
+ return DavErrorCategory.davSyncTokenInvalid;
+ }
+ if (normalizedCode.contains('collectionremoved') ||
+ normalizedCode.contains('collectionnotfound')) {
+ return DavErrorCategory.davCollectionRemoved;
+ }
+
+ if (statusCode != null) {
+ if (statusCode == HttpStatus.unauthorized) {
+ return DavErrorCategory.davAuthRejected;
+ }
+ if (statusCode == HttpStatus.forbidden) {
+ return DavErrorCategory.davPermissionDenied;
+ }
+ if (statusCode == HttpStatus.notFound || statusCode == HttpStatus.gone) {
+ return DavErrorCategory.davCollectionRemoved;
+ }
+ if (statusCode == HttpStatus.conflict ||
+ statusCode == HttpStatus.preconditionFailed ||
+ statusCode == HttpStatus.locked) {
+ return DavErrorCategory.davResourceConflict;
+ }
+ if (statusCode == HttpStatus.tooManyRequests) {
+ return DavErrorCategory.davRateLimited;
+ }
+ if (statusCode == HttpStatus.insufficientStorage) {
+ return DavErrorCategory.davQuotaOrSizeLimit;
+ }
+ if (statusCode >= 500) {
+ return DavErrorCategory.davServerUnavailable;
+ }
+ }
+
+ return switch (kind) {
+ DavErrorKind.cancelled => DavErrorCategory.davOperationCancelled,
+ DavErrorKind.timeout ||
+ DavErrorKind.network => DavErrorCategory.davTransientNetwork,
+ DavErrorKind.tls => DavErrorCategory.davTlsFailure,
+ DavErrorKind.authentication => DavErrorCategory.davAuthRejected,
+ DavErrorKind.authorization => DavErrorCategory.davPermissionDenied,
+ DavErrorKind.rateLimited => DavErrorCategory.davRateLimited,
+ DavErrorKind.invalidSyncToken => DavErrorCategory.davSyncTokenInvalid,
+ DavErrorKind.preconditionFailed ||
+ DavErrorKind.conflict => DavErrorCategory.davResourceConflict,
+ DavErrorKind.uidConflict => DavErrorCategory.davUidConflict,
+ DavErrorKind.invalidCalendarData => DavErrorCategory.davMalformedResource,
+ DavErrorKind.unsupportedComponent =>
+ DavErrorCategory.davUnsupportedComponent,
+ DavErrorKind.maximumResourceSize ||
+ DavErrorKind.limitExceeded ||
+ DavErrorKind.responseTooLarge => DavErrorCategory.davQuotaOrSizeLimit,
+ DavErrorKind.notFound => DavErrorCategory.davCollectionRemoved,
+ DavErrorKind.server => DavErrorCategory.davServerUnavailable,
+ DavErrorKind.redirectRejected ||
+ DavErrorKind.redirectLoop ||
+ DavErrorKind.malformedXml ||
+ DavErrorKind.malformedStatus ||
+ DavErrorKind.protocol => DavErrorCategory.davProtocolViolation,
+ };
+}
+
+/// Parses a server retry hint without including the original header in an
+/// exception or log message.
+Duration? parseDavRetryAfter(
+ String? value, {
+ DateTime? nowUtc,
+ Duration maximum = const Duration(hours: 24),
+}) {
+ final trimmed = value?.trim();
+ if (trimmed == null || trimmed.isEmpty) return null;
+ Duration? result;
+ final seconds = int.tryParse(trimmed);
+ if (seconds != null) {
+ if (seconds < 0) return null;
+ result = Duration(seconds: seconds);
+ } else {
+ try {
+ final deadline = HttpDate.parse(trimmed).toUtc();
+ result = deadline.difference((nowUtc ?? DateTime.now().toUtc()).toUtc());
+ if (result.isNegative) result = Duration.zero;
+ } on Object {
+ return null;
+ }
+ }
+ return result > maximum ? maximum : result;
+}
diff --git a/lib/src/dav/dav_href.dart b/lib/src/dav/dav_href.dart
new file mode 100644
index 0000000..320777b
--- /dev/null
+++ b/lib/src/dav/dav_href.dart
@@ -0,0 +1,54 @@
+import '../providers/busy_provider.dart';
+import 'dav_errors.dart';
+import 'dav_provider_profile.dart';
+
+Uri resolveDavHref({
+ required String href,
+ required Uri responseRequestUri,
+ required DavProviderProfile profile,
+ required Uri accountAuthority,
+ String? correlationId,
+}) {
+ final source = href.trim();
+ if (source.isEmpty) {
+ throw _invalidHref(correlationId);
+ }
+ late final Uri resolved;
+ try {
+ resolved = responseRequestUri.resolve(source);
+ } on FormatException {
+ throw _invalidHref(correlationId);
+ }
+ if (resolved.userInfo.isNotEmpty ||
+ resolved.hasFragment ||
+ resolved.hasQuery ||
+ !profile.isTrustedCredentialDestination(
+ resolved,
+ accountAuthority: accountAuthority,
+ )) {
+ throw _invalidHref(correlationId);
+ }
+ return resolved;
+}
+
+/// Returns the stable account-relative DAV identity without decoding or
+/// re-encoding percent-escaped reserved path octets. iCloud shard hosts are
+/// intentionally excluded; Nextcloud authority is already part of account
+/// identity.
+String normalizedDavHrefKey(BusyProvider provider, Uri requestUri) {
+ if (requestUri.path.isEmpty || !requestUri.path.startsWith('/')) {
+ throw const DavException(
+ kind: DavErrorKind.protocol,
+ code: 'DavHrefMissingAbsolutePath',
+ safeMessage: 'A DAV resource did not have an absolute path.',
+ );
+ }
+ return requestUri.path;
+}
+
+DavException _invalidHref(String? correlationId) => DavException(
+ kind: DavErrorKind.redirectRejected,
+ code: 'DavHrefDestinationRejected',
+ safeMessage: 'The DAV server returned an unsafe resource location.',
+ correlationId: correlationId,
+);
diff --git a/lib/src/dav/dav_provider_profile.dart b/lib/src/dav/dav_provider_profile.dart
new file mode 100644
index 0000000..1a69ddb
--- /dev/null
+++ b/lib/src/dav/dav_provider_profile.dart
@@ -0,0 +1,132 @@
+import '../providers/busy_provider.dart';
+
+const davProviderProfileVersion = 2;
+
+final class DavProviderProfile {
+ const DavProviderProfile({
+ required this.provider,
+ required this.bootstrapUri,
+ required this.calendarEnabled,
+ required this.tasksEnabled,
+ required this.allowCollectionMutations,
+ required this.allowSchedulingMutations,
+ required this.allowMove,
+ this.allowInsecureLoopbackForTesting = false,
+ });
+
+ final BusyProvider provider;
+ final Uri bootstrapUri;
+ final bool calendarEnabled;
+ final bool tasksEnabled;
+ final bool allowCollectionMutations;
+ final bool allowSchedulingMutations;
+ final bool allowMove;
+ final bool allowInsecureLoopbackForTesting;
+
+ bool isTrustedCredentialDestination(
+ Uri destination, {
+ required Uri accountAuthority,
+ }) {
+ if (!_isSafeHttpsUri(destination) &&
+ !(allowInsecureLoopbackForTesting &&
+ _isInsecureLoopbackUri(destination))) {
+ return false;
+ }
+ return switch (provider) {
+ BusyProvider.appleICloud => _isApprovedICloudHost(destination.host),
+ BusyProvider.nextcloud =>
+ _sameOrigin(destination, accountAuthority) &&
+ _preservesInstallationPath(destination, accountAuthority),
+ BusyProvider.google || BusyProvider.microsoft => false,
+ };
+ }
+}
+
+bool _isInsecureLoopbackUri(Uri uri) =>
+ uri.scheme.toLowerCase() == 'http' &&
+ (uri.host == '127.0.0.1' || uri.host == '::1' || uri.host == 'localhost') &&
+ uri.userInfo.isEmpty &&
+ !uri.hasFragment;
+
+DavProviderProfile davProviderProfile(
+ BusyProvider provider, {
+ Uri? nextcloudServer,
+}) => switch (provider) {
+ BusyProvider.appleICloud => DavProviderProfile(
+ provider: provider,
+ bootstrapUri: Uri.parse('https://caldav.icloud.com/'),
+ calendarEnabled: true,
+ tasksEnabled: false,
+ allowCollectionMutations: false,
+ allowSchedulingMutations: false,
+ allowMove: false,
+ ),
+ BusyProvider.nextcloud => DavProviderProfile(
+ provider: provider,
+ bootstrapUri:
+ nextcloudServer ??
+ (throw ArgumentError.value(
+ nextcloudServer,
+ 'nextcloudServer',
+ 'Nextcloud requires the canonical Login Flow v2 server.',
+ )),
+ calendarEnabled: true,
+ tasksEnabled: true,
+ allowCollectionMutations: true,
+ allowSchedulingMutations: false,
+ allowMove: true,
+ ),
+ BusyProvider.google || BusyProvider.microsoft => throw ArgumentError.value(
+ provider,
+ 'provider',
+ 'The provider does not use the DAV transport.',
+ ),
+};
+
+Uri davWellKnownUri(DavProviderProfile profile) {
+ final bootstrap = profile.bootstrapUri;
+ if (profile.provider == BusyProvider.appleICloud) {
+ return bootstrap.replace(
+ path: '/.well-known/caldav',
+ query: null,
+ fragment: null,
+ );
+ }
+ final basePath = bootstrap.path.endsWith('/')
+ ? bootstrap.path
+ : '${bootstrap.path}/';
+ return bootstrap.replace(
+ path: '$basePath.well-known/caldav',
+ query: null,
+ fragment: null,
+ );
+}
+
+bool _isSafeHttpsUri(Uri uri) =>
+ uri.scheme.toLowerCase() == 'https' &&
+ uri.host.isNotEmpty &&
+ uri.userInfo.isEmpty &&
+ !uri.hasFragment;
+
+bool _sameOrigin(Uri left, Uri right) =>
+ left.scheme.toLowerCase() == right.scheme.toLowerCase() &&
+ left.host.toLowerCase() == right.host.toLowerCase() &&
+ left.port == right.port;
+
+bool _preservesInstallationPath(Uri destination, Uri authority) {
+ var basePath = authority.path;
+ if (basePath.isEmpty || basePath == '/') {
+ return true;
+ }
+ while (basePath.endsWith('/')) {
+ basePath = basePath.substring(0, basePath.length - 1);
+ }
+ return destination.path == basePath ||
+ destination.path.startsWith('$basePath/');
+}
+
+bool _isApprovedICloudHost(String value) {
+ final host = value.toLowerCase();
+ return host == 'caldav.icloud.com' ||
+ RegExp(r'^p[0-9]+-caldav[.]icloud[.]com$').hasMatch(host);
+}
diff --git a/lib/src/dav/discovery/dav_discovery_models.dart b/lib/src/dav/discovery/dav_discovery_models.dart
new file mode 100644
index 0000000..a068089
--- /dev/null
+++ b/lib/src/dav/discovery/dav_discovery_models.dart
@@ -0,0 +1,118 @@
+import '../../providers/busy_provider.dart';
+import '../../providers/provider_capabilities.dart';
+
+const davCapabilitiesSchemaVersion = 1;
+const davComponentEvent = 1 << 0;
+const davComponentTodo = 1 << 1;
+const davComponentTimezone = 1 << 2;
+const davComponentJournal = 1 << 3;
+const davComponentFreeBusy = 1 << 4;
+
+enum DavCollectionKind {
+ writableEventCalendar,
+ readOnlyEventCalendar,
+ writableTaskList,
+ readOnlyTaskList,
+ mixedCalendar,
+ subscribedCalendar,
+ schedulingInbox,
+ schedulingOutbox,
+ notifications,
+ unsupported,
+}
+
+final class DavServiceDiscovery {
+ const DavServiceDiscovery({
+ required this.canonicalServiceUri,
+ required this.canonicalOrigin,
+ required this.principalHref,
+ required this.calendarHomeHref,
+ required this.calendarUserAddresses,
+ required this.scheduleInboxHref,
+ required this.scheduleOutboxHref,
+ required this.capabilities,
+ required this.discoveredAtUtc,
+ required this.lastValidatedAtUtc,
+ required this.providerProfileVersion,
+ });
+
+ final Uri canonicalServiceUri;
+ final Uri canonicalOrigin;
+ final Uri principalHref;
+ final Uri calendarHomeHref;
+ final List calendarUserAddresses;
+ final Uri? scheduleInboxHref;
+ final Uri? scheduleOutboxHref;
+ final AccountServiceCapabilities capabilities;
+ final DateTime discoveredAtUtc;
+ final DateTime lastValidatedAtUtc;
+ final int providerProfileVersion;
+}
+
+final class DavCollectionDiscovery {
+ const DavCollectionDiscovery({
+ required this.hrefKey,
+ required this.requestUri,
+ required this.displayName,
+ required this.description,
+ required this.resourceTypes,
+ required this.supportedComponentMask,
+ required this.supportedCalendarData,
+ required this.supportedReports,
+ required this.currentUserPrivileges,
+ required this.ownerHref,
+ required this.safeDisplayMetadata,
+ required this.color,
+ required this.sortOrder,
+ required this.calendarTimeZone,
+ required this.calendarTimeZoneId,
+ required this.scheduleTransparency,
+ required this.maximumResourceSize,
+ required this.maximumInstances,
+ required this.syncToken,
+ required this.ctag,
+ required this.capabilities,
+ required this.kind,
+ required this.eventProjectionEnabled,
+ required this.taskProjectionEnabled,
+ });
+
+ final String hrefKey;
+ final Uri requestUri;
+ final String displayName;
+ final String? description;
+ final Set resourceTypes;
+ final int supportedComponentMask;
+ final List