Feat/to me it looks like there are lots of 60dd04 (#7280)

* fix(issue): prevent crash from orphan issueProviderId (#7135)

The Jira image-headers effect in task-detail-panel subscribed to
selectIssueProviderById without an error handler, so a task with an
issueProviderId pointing at a deleted provider (e.g. after sync
convergence where taskIdsToUnlink didn't cover all local tasks)
propagated the selector throw to Zone.js as a crash dialog. Wrap the
inner selector observable in catchError that logs and falls back to
of(null); the downstream jiraCfg?.isEnabled guard handles the fallback.

Also drop IssueLog.log(issueProviderKey, issueProvider) from the
throwing variant of the selector: providers may carry credentials
(host, token, apiKey) and IssueLog history is exportable.

* fix(focus-mode): sync tray countdown with in-app timer during breaks

Tray title was rebuilt from a cached currentFocusSessionTime that only
refreshed when CURRENT_TASK_UPDATED fired. addTimeSpent is gated on an
active current task, so during focus-mode breaks or task-less focus
sessions the cache froze while the in-app timer kept ticking.

Add the tick action to taskChangeElectron$ so the cache refreshes every
second whenever the focus timer is running.

Fixes #7278

* fix(ci): restore GitHub Actions SHA pins undone by 0e9218bd68

Commit 0e9218bd68 silently reverted PR #7212 (github-actions-minor group
bump) along with its stated sync/client-id work. This restores the 15
workflow files to their pre-revert state.

Actions restored to newer pinned SHAs:
- actions/upload-artifact v7.0.0 -> v7.0.1
- step-security/harden-runner v2.16.1 -> v2.17.0
- softprops/action-gh-release v2.6.1 -> v3.0.0
- signpath/github-action-submit-signing-request v2.0 -> v2.1
- anthropics/claude-code-action v1.0.89 -> v1.0.93
- docker/build-push-action v7.0.0 -> v7.1.0
- easingthemes/ssh-deploy v5.1.1 -> v6.0.3

* fix: restore i18n, UI, and docs work undone by 0e9218bd68

Commit 0e9218bd68 silently reverted the following work alongside its stated
sync/client-id changes. Files where later master commits (fec7b25f23, etc.)
already re-applied the reverted work are intentionally left untouched.

Restored:
- #7232 docs/long-term-plans/location-based-reminders.md (513 lines)
- #7199 Romanian i18n phase 3 (ro.json + ro-md.json, ~1168 lines)
- #7049 Polish translation improvements
- #7143 planner component styling (4 scss files)
- #7211 add-task-bar preserve time estimate when typing title
- #7208 task.reducer roll-up estimates for added subtasks
- #6767 focus-mode pomodoro reset button
- #7205 plugin-dev github-issue-provider TOKEN description
- #7231 mobile-bottom-nav FAB fix
- a4fe03272 iOS keyboard accessory bar (global-theme + dialog-fullscreen-markdown)
- 309670db3 ShortSyntaxEffects undefined guard
- 667a7986f Dropbox PKCE auth comment/behavior

* fix(electron): restore electron + e2e work undone by 0e9218bd68

Commit 0e9218bd68 silently reverted the following electron/e2e work.
Files already re-fixed by later master commits are left as-is:
- e2e/tests/sync/supersync-archive-conflict.spec.ts (de33234976 + 191d129ff3)

Restored:
- e8a3e156eb fix(electron): Linux autostart IDB backing-store recovery
  (re-adds electron/clear-stale-idb-locks.ts + start-app.ts wiring)
- 5ce78a5b63 fix(electron): macOS Cmd+Q / Dock > Quit hang
  (setIsQuiting + before-quit delegate to close-handler)
- 46e0fa2d01 fix(sync): FILE_SYNC_LIST_FILES IPC contract
  (electronAPI.d.ts + local-file-sync + preload + ipc-events)
- ea1ef16307 fix(android): session-only SAF permissions on OEM devices
- 8865dc0a50 test(e2e): supersync parallel-worker stampede guard
  (SUPERSYNC_SERVER_HEALTHY env-var fallback + goto retry loop)
- af7c7687e2 test(e2e): block WS-triggered downloads in non-WS specs
- 265b44db5d test(e2e): premature waitForURL on daily-summary
  (this is literally the fix the bad commit's message claimed to add)

Conflict resolutions:
- e2e/utils/supersync-helpers.ts: kept the refined getDoneTaskElement
  checks from d64014d086 (later than c558bcab5e) while restoring the
  goto retry loop from 8865dc0a50.
- electron/start-app.ts: unioned imports (setIsQuiting +
  clearStaleLevelDbLocks from theirs, fs from ours).

* fix(sync): restore sync-core work undone by 0e9218bd68

Commit 0e9218bd68 silently reverted parts of several sync fixes. Most
sync-core reverts have already been re-addressed differently on master
by later commits (1f5184f6e7, 05cd875dd6, 09f5ced2c9, 7df43358ab,
d9158d6adb, 32dbc95ed9, 8c3b08e016, f89fe1ebc3) — those files are
intentionally left untouched to avoid reverting master's newer work.

This PR restores only the pieces that are genuinely still missing:

- e8a3e156eb fix(electron): IDB backing-store autoreload (in-app piece)
  operation-log-hydrator.service.ts + .spec.ts (the electron/clear-
  stale-idb-locks.ts piece was restored in the prior commit)

Plus three low-risk documentation/cleanup restorations:
- operation-sync.util.ts — add "Nextcloud" to isFileBasedProvider JSDoc
- dropbox.ts — restore improved _getRedirectUri JSDoc (667a7986fb)
- dialog-get-and-enter-auth-code.component.ts — restore isNativePlatform
  comment explaining why manual code entry flow is used (667a7986fb)
- file-adapter.interface.ts — remove stray "// NEW" comment (46e0fa2d01)

Intentionally NOT restored (master's newer work covers or supersedes):
- sync-trigger.service.ts / sync.effects.ts (05cd875dd6)
- sync-wrapper.service.ts (1f5184f6e7)
- sync-errors.ts (1f5184f6e7 re-added LegacySyncFormatDetectedError)
- file-based-sync-adapter.service.ts + spec (1f5184f6e7 + d9158d6adb)
- file-based-sync.types.ts (1f5184f6e7)
- operation-log.const.ts (32dbc95ed9 bumped IDB_OPEN_RETRIES to 5)
- dialog-sync-initial-cfg.component.ts (f89fe1ebc3)
This commit is contained in:
Johannes Millan 2026-04-20 12:04:38 +02:00 committed by GitHub
parent 1d98d356be
commit 01e30b9c7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 1719 additions and 684 deletions

View file

@ -13,7 +13,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
allowed-endpoints: >

View file

@ -77,7 +77,7 @@ jobs:
# APK is now signed automatically by Gradle using signingConfig
- name: 'Upload APK files'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sup-android-release
path: android/app/build/outputs/apk/**/*.apk

View file

@ -77,7 +77,7 @@ jobs:
github_token: ${{ secrets.github_token }}
- name: 'Upload Artifact'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: WinStoreRelease
path: .tmp/app-builds/*.appx

View file

@ -200,7 +200,7 @@ jobs:
run: ls -la "$RUNNER_TEMP/ipa-output"
- name: Upload IPA artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sup-ios-release
path: ${{ runner.temp }}/ipa-output/*.ipa

View file

@ -15,7 +15,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
allowed-endpoints: >

View file

@ -15,7 +15,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
allowed-endpoints: >
@ -69,7 +69,7 @@ jobs:
run: npm run buildFrontend:prodWeb
- name: Deploy to Web Server
uses: easingthemes/ssh-deploy@a1aa0b6cf96ce2406eef90faa35007a4a7bf0ac0 # v5.1.1
uses: easingthemes/ssh-deploy@2cc5b27bf3029d0455dd5e09fe02633904031447 # v6.0.3
env:
SSH_PRIVATE_KEY: ${{ secrets.WEB_SERVER_SSH_KEY }}
ARGS: '-rltgoDzvO --delete --exclude "news.json"'

View file

@ -31,7 +31,7 @@ jobs:
fi
} > /tmp/release-body.md
- name: Create Draft Release
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
tag_name: ${{ github.ref_name }}
draft: true
@ -102,7 +102,7 @@ jobs:
- name: 'Upload E2E results on failure'
if: ${{ failure() }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2eResults
path: .tmp/e2e-test-results/**/*.*
@ -314,7 +314,7 @@ jobs:
- name: Upload unsigned executables for SignPath
id: upload-unsigned
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: unsigned-windows-executables
path: .tmp/app-builds/*.exe
@ -322,7 +322,7 @@ jobs:
retention-days: 1
- name: Sign Windows executables with SignPath
uses: signpath/github-action-submit-signing-request@3f9250c56651ff692d6729a2fbb0603a42d7d322 # v2
uses: signpath/github-action-submit-signing-request@bc66d86b015a46e9c6d9700de73143a82f9570ff # v2
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
@ -474,7 +474,7 @@ jobs:
"
- name: Publish signed Windows binaries to GitHub Release
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
files: |

View file

@ -120,7 +120,7 @@ jobs:
run: npm run e2e:ci
- name: 'Upload E2E results on failure'
if: ${{ failure() }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2eResults
path: .tmp/e2e-test-results/**/*.*

View file

@ -32,7 +32,7 @@ jobs:
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@6e2bd52842c65e914eba5c8badd17560bd26b5de # v1
uses: anthropics/claude-code-action@b47fd721da662d48c5680e154ad16a73ed74d2e0 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -50,7 +50,7 @@ jobs:
test -f .tmp/angular-dist/browser/assets/bundled-plugins/api-test-plugin/manifest.json || (echo "Plugin assets missing from build" && exit 1)
- name: Upload Build Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-build-${{ github.run_id }}
path: .tmp/angular-dist
@ -86,7 +86,7 @@ jobs:
- name: Upload E2E Results on Failure
if: ${{ failure() }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-results-regular-${{ github.run_id }}
path: .tmp/e2e-test-results/**/*.*
@ -143,7 +143,7 @@ jobs:
- name: Upload E2E Results on Failure
if: ${{ failure() }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-results-webdav-${{ github.run_id }}
path: .tmp/e2e-test-results/**/*.*
@ -205,7 +205,7 @@ jobs:
- name: Upload E2E Results on Failure
if: ${{ failure() }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-results-supersync-${{ matrix.shard }}-${{ github.run_id }}
path: .tmp/e2e-test-results/**/*.*

View file

@ -62,7 +62,7 @@ jobs:
release: false
- name: 'Upload Artifact'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: WinBuildStuff
path: .tmp/app-builds/*.exe
@ -162,7 +162,7 @@ jobs:
# if: always()
# run: ls -la && cat notarization-error.log
- name: 'Upload Artifact'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dmg
path: .tmp/app-builds/*.dmg

View file

@ -41,7 +41,7 @@ jobs:
run: npm run buildFrontend:prodWeb
- name: Upload build artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pr-preview-build
path: dist/browser

View file

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
allowed-endpoints: >
@ -55,7 +55,7 @@ jobs:
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build and push Docker image
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
push: true

View file

@ -19,7 +19,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
allowed-endpoints: >
@ -59,7 +59,7 @@ jobs:
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build and push Docker image
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
file: ./packages/super-sync-server/Dockerfile

View file

@ -117,7 +117,7 @@ jobs:
fi
- name: Upload DMG artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: mac-dmg-build
path: .tmp/app-builds/*.dmg

View file

@ -2,7 +2,9 @@ package com.superproductivity.superproductivity.plugins
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Process
import android.provider.DocumentsContract
import androidx.activity.result.ActivityResult
import androidx.documentfile.provider.DocumentFile
@ -40,18 +42,26 @@ class SafBridgePlugin : Plugin() {
@ActivityCallback
private fun handleFolderSelectionResult(call: PluginCall, result: ActivityResult) {
if (result.resultCode == Activity.RESULT_OK) {
val uri = result.data?.data
val resultData = result.data
val uri = resultData?.data
if (uri != null) {
// Take persistable permission
// Use only the flags that were actually granted by the result intent.
// Applying flags not present in the result throws SecurityException on some devices.
// resultData is Intent? but uri != null proves resultData is non-null here;
// Kotlin cannot infer this transitively, so !! is required and safe.
val grantedFlags = resultData!!.flags and
(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
try {
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
context.contentResolver.takePersistableUriPermission(uri, grantedFlags)
} catch (e: SecurityException) {
// Some devices/Android versions fail to persist SAF permission
android.util.Log.w("SafBridgePlugin", "Failed to persist URI permission: ${e.message}")
call.reject("Failed to persist folder permission: ${e.message}")
return
// Some devices/OEM ROMs fail to persist SAF permission even when
// FLAG_GRANT_PERSISTABLE_URI_PERMISSION was requested. Fall through and
// still return the URI — it will work for this session even if it can't
// be persisted across reboots (user will need to re-select next launch).
android.util.Log.w(
"SafBridgePlugin",
"Could not persist URI permission (session-only access): ${e.message}",
)
}
val ret = JSObject()
@ -208,8 +218,19 @@ class SafBridgePlugin : Plugin() {
try {
val uri = Uri.parse(uriString)
val persistedUris = context.contentResolver.persistedUriPermissions
val hasPermission = persistedUris.any { it.uri == uri && it.isReadPermission && it.isWritePermission }
// Check both persistent grants (survive reboots) and temporary session grants
// (active for current app session only). This is required to support devices/OEM
// ROMs where takePersistableUriPermission throws SecurityException — on those
// devices the session grant is still valid even though it could not be persisted.
val pid = Process.myPid()
val uid = Process.myUid()
val canRead = context.checkUriPermission(
uri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION,
) == PackageManager.PERMISSION_GRANTED
val canWrite = context.checkUriPermission(
uri, pid, uid, Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
) == PackageManager.PERMISSION_GRANTED
val hasPermission = canRead && canWrite
val ret = JSObject()
ret.put("hasPermission", hasPermission)

View file

@ -0,0 +1,513 @@
# Location-Based Reminders — Design Document
> **Status: Planned (Brainstorm)**
## Overview
Add location-based reminders to Super Productivity. Users can attach a saved location to a task and receive a notification when they arrive at that place. This is primarily a mobile feature (Android/iOS via Capacitor), with passive location display on desktop/web.
## Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Primary platform | Mobile (Android/iOS) | Geofencing requires GPS + background location. Desktop/web show location info but don't trigger. |
| Trigger type | Arrive only (v1) | Simplest. Leave triggers can be added later. |
| Location management | Saved locations entity | Users revisit the same places. Follows existing Tag entity pattern. |
| Location picker (v1) | "Use current location" + label | No map needed for v1. Map picker is a v2 enhancement. |
| Geofencing approach | Custom native Capacitor plugin | `@capacitor/geolocation` only does point-in-time reads, not geofencing. Need native `GeofencingClient` (Android) / `CLLocationManager` (iOS). |
| Sync | Sync by default | Location data treated like any other entity. Each device manages its own geofences locally after sync. |
| Feature toggle | `isLocationRemindersEnabled` in `AppFeaturesConfig` | Opt-in, default false. |
---
## 1. Data Model
### 1.1 SavedLocation Entity
New file: `src/app/features/saved-location/saved-location.model.ts`
```typescript
import { EntityState } from '@ngrx/entity';
export interface SavedLocationCopy {
id: string;
title: string; // "Office", "Grocery Store", "Gym"
lat: number; // latitude
lng: number; // longitude
radius: number; // geofence radius in meters (default 200)
icon?: string | null; // material icon name, e.g. 'home', 'work', 'shopping_cart'
created: number; // creation timestamp
modified?: number; // last update timestamp
}
export type SavedLocation = Readonly<SavedLocationCopy>;
export type SavedLocationState = EntityState<SavedLocation>;
```
### 1.2 Task Model Changes
File: `src/app/features/tasks/task.model.ts` — add to `TaskCopy`:
```typescript
/** ID of a SavedLocation. When set, a geofence reminder is active for this task. */
locationReminderId?: string | null;
```
### 1.3 Config Changes
File: `src/app/features/config/global-config.model.ts` — add to `AppFeaturesConfig`:
```typescript
isLocationRemindersEnabled: boolean; // default false
```
File: `src/app/features/config/default-global-config.const.ts` — set default:
```typescript
isLocationRemindersEnabled: false,
```
### 1.4 Platform Capabilities
File: `src/app/core/platform/platform-capabilities.model.ts` — add:
```typescript
/** Whether the platform supports native geofencing (background location monitoring). */
readonly geofencing: boolean;
```
| Platform | `geofencing` |
|----------|-------------|
| Android (`ANDROID_CAPABILITIES`) | `true` |
| iOS (`IOS_CAPABILITIES`) | `true` |
| Electron (`ELECTRON_CAPABILITIES`) | `false` |
| Web (`WEB_CAPABILITIES`) | `false` |
---
## 2. Sync & Persistence Registration
All registration steps required for the new entity, following existing patterns:
### 2.1 Entity Type
File: `packages/shared-schema/src/entity-types.ts`
Add `'SAVED_LOCATION'` to the `ENTITY_TYPES` array.
### 2.2 Action Types Enum
File: `src/app/op-log/core/action-types.enum.ts`
```typescript
// SavedLocation actions
SAVED_LOCATION_ADD = '[SavedLocation] Add SavedLocation',
SAVED_LOCATION_UPDATE = '[SavedLocation] Update SavedLocation',
SAVED_LOCATION_DELETE = '[SavedLocation] Delete SavedLocation',
```
These string values are **immutable** once deployed — they are used for encoding/decoding operations in IndexedDB and sync between clients.
### 2.3 Entity Registry
File: `src/app/op-log/core/entity-registry.ts`
Add to `ENTITY_CONFIGS`:
```typescript
SAVED_LOCATION: {
storagePattern: 'adapter',
featureName: SAVED_LOCATION_FEATURE_NAME,
payloadKey: 'savedLocation',
adapter: savedLocationAdapter,
selectEntities: createSelector(
selectSavedLocationFeatureState,
selectSavedLocationEntitiesFromAdapter,
),
selectById: selectSavedLocationById,
},
```
### 2.4 Model Config
File: `src/app/op-log/model/model-config.ts`
Add to `AllModelConfig` type:
```typescript
savedLocation: ModelCfg<SavedLocationState>;
```
Add to `MODEL_CONFIGS`:
```typescript
savedLocation: {
defaultData: initialSavedLocationState,
isMainFileModel: true,
repair: fixEntityStateConsistency,
},
```
### 2.5 Root State
File: `src/app/root-store/root-state.ts`
```typescript
[SAVED_LOCATION_FEATURE_NAME]: SavedLocationState;
```
### 2.6 Feature Store Registration
File: `src/app/root-store/feature-stores.module.ts`
```typescript
StoreModule.forFeature(SAVED_LOCATION_FEATURE_NAME, savedLocationReducer),
```
### 2.7 Cascading Delete Meta-Reducer
File: `src/app/root-store/meta/task-shared-meta-reducers/saved-location-shared.reducer.ts`
When a SavedLocation is deleted, clear `locationReminderId` on all tasks that reference it. This must be a meta-reducer (not an effect) to ensure atomicity — one operation in the sync log.
Register in `src/app/root-store/meta/meta-reducer-registry.ts` in **Phase 5** (Entity-Specific Cascades), alongside `tagSharedMetaReducer`, `projectSharedMetaReducer`, etc.
---
## 3. NgRx Store
### 3.1 Actions
File: `src/app/features/saved-location/store/saved-location.actions.ts`
Following the Tag action pattern with `PersistentActionMeta`:
| Action | OpType | Payload |
|--------|--------|---------|
| `addSavedLocation` | `Create` | `{ savedLocation: SavedLocation }` |
| `updateSavedLocation` | `Update` | `{ savedLocation: Update<SavedLocation> }` |
| `deleteSavedLocation` | `Delete` | `{ id: string }` |
All include `meta: { isPersistent: true, entityType: 'SAVED_LOCATION', entityId, opType } satisfies PersistentActionMeta`.
### 3.2 Reducer
File: `src/app/features/saved-location/store/saved-location.reducer.ts`
Standard `@ngrx/entity` adapter:
```typescript
export const SAVED_LOCATION_FEATURE_NAME = 'savedLocation';
export const savedLocationAdapter = createEntityAdapter<SavedLocation>({
sortComparer: (a, b) => a.title.localeCompare(b.title),
});
export const initialSavedLocationState = savedLocationAdapter.getInitialState();
```
### 3.3 Selectors
File: `src/app/features/saved-location/store/saved-location.selectors.ts`
| Selector | Returns |
|----------|---------|
| `selectSavedLocationFeatureState` | Feature state |
| `selectAllSavedLocations` | `SavedLocation[]` |
| `selectSavedLocationById` | `SavedLocation \| undefined` |
| `selectSavedLocationEntities` | `Dictionary<SavedLocation>` |
Task-side selector (in task selectors or a cross-feature selector):
| Selector | Returns |
|----------|---------|
| `selectTasksWithLocationReminder` | All undone tasks that have `locationReminderId` set |
### 3.4 Service
File: `src/app/features/saved-location/saved-location.service.ts`
Thin wrapper dispatching actions to the store. Methods: `addSavedLocation()`, `updateSavedLocation()`, `deleteSavedLocation()`, `getById$()`.
---
## 4. Geofencing Service
### 4.1 Architecture
New file: `src/app/features/saved-location/geofence.service.ts`
```
Store (undone tasks with locationReminderId)
→ GeofenceService watches selector (distinctUntilChanged)
→ Computes which locations need active geofences
→ Registers/unregisters via custom Capacitor plugin
→ Receives geofence enter events
→ Emits to ReminderService for dialog/notification
```
### 4.2 Why a Custom Capacitor Plugin
`@capacitor/geolocation` only provides one-time and continuous position reads — it does **not** support geofencing. Native geofencing requires:
- **Android:** `com.google.android.gms.location.GeofencingClient` (Google Play Services). Supports up to 100 geofences. Fires `BroadcastReceiver` on enter/exit.
- **iOS:** `CLLocationManager.startMonitoring(for: CLCircularRegion)`. Supports up to 20 monitored regions. Fires delegate callbacks on enter/exit.
Implementation: Create `GeofencePlugin.kt` (Android) and `GeofencePlugin.swift` (iOS) extending Capacitor's `Plugin` class. Register in `CapacitorMainActivity.onCreate()` following the pattern of `SafBridgePlugin`, `WebDavHttpPlugin`, etc.
### 4.3 Lifecycle Rules
**Register geofence when:**
- Feature is enabled + a task gets `locationReminderId` assigned
- App starts with existing location-reminded tasks
**Unregister geofence when:**
- Task is completed, deleted, or `locationReminderId` cleared
- SavedLocation is deleted
- No more undone tasks reference that location
- Feature is disabled
**iOS 20-region limit:** Only register geofences for the 20 locations with the most active tasks. Re-evaluate when tasks change.
### 4.4 Effects
Effects MUST use `inject(LOCAL_ACTIONS)` — geofence registration should never happen during remote sync replay. Each device manages its own geofences based on local state after sync.
### 4.5 Background Behavior
When geofence fires while app is in background:
- **Android:** `BroadcastReceiver` shows native notification directly (same pattern as `ReminderAlarmReceiver`). Tapping opens app + reminder dialog.
- **iOS:** `CLLocationManager` delegate fires local notification. Tapping opens app + reminder dialog.
### 4.6 Permission Flow
On first use:
1. Check `CapacitorPlatformService.hasCapability('geofencing')`
2. Request `ACCESS_FINE_LOCATION` + `ACCESS_BACKGROUND_LOCATION` (Android) or "Always" location access (iOS)
3. If denied: feature degrades to display-only, show explanation
---
## 5. Reminder Integration
### 5.1 Integration Point
The existing `ReminderService.onRemindersActive$` is a derived observable — external code cannot emit to it directly. Two approaches:
**Option A (recommended):** Add a new public subject on `ReminderService`:
```typescript
// In ReminderService
private _onLocationReminders$ = new Subject<TaskWithReminderData[]>();
locationReminders$ = this._onLocationReminders$.asObservable();
```
Then merge in `ReminderModule`:
```typescript
merge(
this._reminderService.onRemindersActive$,
this._reminderService.locationReminders$,
).subscribe(reminders => /* existing dialog handling */);
```
**Option B:** Add a public method `emitLocationReminders()` that pushes to the private `_onRemindersActive$` subject.
### 5.2 Data Shape
Location-triggered reminders must satisfy `TaskWithReminderData`:
```typescript
interface TaskWithReminderData extends Task {
readonly reminderData: { remindAt: number }; // use trigger timestamp
readonly parentData?: Task;
readonly isDeadlineReminder?: boolean; // false for location
}
```
Consider adding `isLocationReminder?: boolean` for UI differentiation.
### 5.3 Dialog UX Changes
The existing `DialogViewTaskRemindersComponent` works as-is with minor additions:
- Show location name/icon in header (e.g., "At: Grocery Store")
- **Snooze** = suppress this location reminder for 1 hour (temporary geofence pause)
- **Dismiss** = clear `locationReminderId` on the task
- **Done** = mark task complete (same as today)
- Hide "Edit Reminder" time picker for location-triggered reminders
### 5.4 Native Notifications (Background)
Reuse `CapacitorReminderService` patterns:
- Android: native notification via `ReminderNotificationHelper` triggered from geofence `BroadcastReceiver`
- iOS: `LocalNotifications.schedule()` triggered from `CLLocationManager` delegate
- Both support Done/Snooze action buttons
---
## 6. UI Components
### 6.1 New Components
| Component | Path | Purpose |
|-----------|------|---------|
| `saved-location-settings` | `src/app/features/saved-location/saved-location-settings/` | CRUD list in settings page |
| `location-picker-dialog` | `src/app/features/saved-location/location-picker-dialog/` | Assign location to task (dropdown + "Use current location") |
### 6.2 Modified Components
| Component | Change |
|-----------|--------|
| Task detail panel | Add "Location" field showing assigned location |
| Task schedule dialog | Add location option alongside time-based reminder |
| Settings page | Add "Locations" section |
| Reminder dialog | Show location name when triggered by geofence |
### 6.3 Location Picker UX
```
┌─────────────────────────────────┐
│ Set Location Reminder │
├─────────────────────────────────┤
│ │
│ 📍 Use Current Location │ ← gets GPS, prompts for label
│ │
│ ─── Saved Locations ────────── │
│ │
│ 🏠 Home │
│ 🏢 Office │
│ 🛒 Grocery Store │
│ │
│ [ Remove Location ] [ Cancel ] │
└─────────────────────────────────┘
```
### 6.4 Settings Section
```
┌─────────────────────────────────┐
│ Location Reminders │
├─────────────────────────────────┤
│ [Toggle] Enable location │
│ reminders │
│ │
│ Saved Locations: │
│ ┌─────────────────────────┐ │
│ │ 🏠 Home 200m [✏️][🗑]│ │
│ │ 🏢 Office 150m [✏️][🗑]│ │
│ │ 🛒 Grocery 200m [✏️][🗑]│ │
│ └─────────────────────────┘ │
│ [ + Add Location ] │
└─────────────────────────────────┘
```
---
## 7. Cross-Platform Behavior
| Platform | Geofencing | Location Display | Notifications |
|----------|-----------|-----------------|---------------|
| **Android** | Native `GeofencingClient`, up to 100 fences, background | Yes | Native via `BroadcastReceiver` |
| **iOS** | Native `CLLocationManager`, up to 20 regions, background | Yes | Native via `LocalNotifications` |
| **Electron** | None | Yes (label only) | None |
| **Web** | None | Yes (label only) | None |
---
## 8. File Structure
```
src/app/features/saved-location/
├── saved-location.model.ts
├── saved-location.const.ts # DEFAULT_RADIUS = 200
├── saved-location.service.ts
├── geofence.service.ts # Capacitor geofencing bridge
├── store/
│ ├── saved-location.actions.ts
│ ├── saved-location.reducer.ts
│ └── saved-location.selectors.ts
├── saved-location-settings/
│ ├── saved-location-settings.component.ts
│ └── saved-location-settings.component.html
└── location-picker-dialog/
├── location-picker-dialog.component.ts
└── location-picker-dialog.component.html
src/app/root-store/meta/task-shared-meta-reducers/
└── saved-location-shared.reducer.ts # Cascading delete
android/app/src/main/java/.../
└── GeofencePlugin.kt # Native Android geofencing
GeofenceBroadcastReceiver.kt # Handles fence enter events
ios/App/App/
└── GeofencePlugin.swift # Native iOS geofencing
```
---
## 9. All Files to Touch
### New Files
| File | Purpose |
|------|---------|
| `src/app/features/saved-location/saved-location.model.ts` | Entity interface |
| `src/app/features/saved-location/saved-location.const.ts` | Defaults |
| `src/app/features/saved-location/saved-location.service.ts` | Service |
| `src/app/features/saved-location/geofence.service.ts` | Capacitor bridge |
| `src/app/features/saved-location/store/saved-location.actions.ts` | Actions |
| `src/app/features/saved-location/store/saved-location.reducer.ts` | Reducer + adapter |
| `src/app/features/saved-location/store/saved-location.selectors.ts` | Selectors |
| `src/app/features/saved-location/saved-location-settings/*` | Settings UI |
| `src/app/features/saved-location/location-picker-dialog/*` | Picker dialog |
| `src/app/root-store/meta/task-shared-meta-reducers/saved-location-shared.reducer.ts` | Cascading deletes |
| `android/.../GeofencePlugin.kt` | Android native geofencing |
| `android/.../GeofenceBroadcastReceiver.kt` | Android fence event handler |
| `ios/App/App/GeofencePlugin.swift` | iOS native geofencing |
### Modified Files
| File | Change |
|------|--------|
| `packages/shared-schema/src/entity-types.ts` | Add `'SAVED_LOCATION'` |
| `src/app/op-log/core/action-types.enum.ts` | Add `SAVED_LOCATION_ADD/UPDATE/DELETE` |
| `src/app/op-log/core/entity-registry.ts` | Add `SAVED_LOCATION` config |
| `src/app/op-log/model/model-config.ts` | Add to `AllModelConfig` + `MODEL_CONFIGS` |
| `src/app/root-store/root-state.ts` | Add to `RootState` |
| `src/app/root-store/feature-stores.module.ts` | Register feature store |
| `src/app/root-store/meta/meta-reducer-registry.ts` | Register cascading delete in Phase 5 |
| `src/app/features/tasks/task.model.ts` | Add `locationReminderId` field |
| `src/app/features/config/global-config.model.ts` | Add `isLocationRemindersEnabled` |
| `src/app/features/config/default-global-config.const.ts` | Set default `false` |
| `src/app/core/platform/platform-capabilities.model.ts` | Add `geofencing` capability |
| `src/app/features/reminder/reminder.service.ts` | Add `locationReminders$` subject |
| `src/app/features/reminder/reminder.module.ts` | Merge location reminders into dialog flow |
| `android/app/src/main/AndroidManifest.xml` | Add location permissions |
| `android/.../CapacitorMainActivity.kt` | Register `GeofencePlugin` |
---
## 10. Future Enhancements (Out of Scope)
- Map picker with OpenStreetMap tiles
- Address search / geocoding (Nominatim)
- Leave triggers ("remind when I leave the office")
- Time + location combos ("at the store, but only after 5 PM")
- Project default locations
- Location-based task views ("show tasks for where I am now")
- WiFi-based triggers (alternative to GPS, works indoors)
- Bluetooth beacon triggers
---
## 11. Verification Plan
1. **Unit tests:** SavedLocation reducer, selectors, service, cascading delete meta-reducer
2. **Manual mobile testing:**
- Create location from "Use current location"
- Assign to a task, verify geofence registration
- Move in/out of geofence, verify notification fires
- Complete task, verify geofence unregistered
- Delete location, verify `locationReminderId` cleared on tasks
3. **Desktop/web:** Verify location label displays on tasks, no geofencing attempted
4. **Sync:** Create location on device A, verify it appears on device B
5. **Lint/format:** `npm run lint`, `npm run prettier`, `npm test`

View file

@ -47,9 +47,18 @@ export const test = base.extend<SuperSyncFixtures>({
* Also checks server health and skips test if server unavailable.
*/
testRunId: async ({}, use, testInfo) => {
// Check server health once per worker
// Check server health once per worker.
// Prefer the result pre-computed in globalSetup (stored as an env var before workers
// were forked) to avoid a stampede of concurrent HTTP requests from all workers
// simultaneously — which can overload the server and produce false negatives.
if (serverHealthyCache === null) {
serverHealthyCache = await isServerHealthy();
if (process.env.SUPERSYNC_SERVER_HEALTHY !== undefined) {
serverHealthyCache = process.env.SUPERSYNC_SERVER_HEALTHY === 'true';
} else {
// Fallback for when tests are run directly without going through globalSetup
// (e.g., `npx playwright test --config ... e2e/tests/sync/foo.spec.ts`)
serverHealthyCache = await isServerHealthy();
}
if (!serverHealthyCache) {
console.warn(
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
@ -69,9 +78,13 @@ export const test = base.extend<SuperSyncFixtures>({
* Tests are automatically skipped if the server is not healthy.
*/
serverHealthy: async ({}, use, testInfo) => {
// Only check once per worker
// Only check once per worker — use globalSetup env var if available (see testRunId above)
if (serverHealthyCache === null) {
serverHealthyCache = await isServerHealthy();
if (process.env.SUPERSYNC_SERVER_HEALTHY !== undefined) {
serverHealthyCache = process.env.SUPERSYNC_SERVER_HEALTHY === 'true';
} else {
serverHealthyCache = await isServerHealthy();
}
if (!serverHealthyCache) {
console.warn(
'SuperSync server not healthy at http://localhost:1901 - skipping tests',

View file

@ -2,6 +2,7 @@ import { FullConfig } from '@playwright/test';
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { isServerHealthy } from './utils/supersync-helpers';
/**
* Warm up the dev server by fetching the app once before any tests start.
@ -69,6 +70,20 @@ const globalSetup = async (config: FullConfig): Promise<void> => {
config.projects[0]?.use?.baseURL ||
'http://localhost:4242';
await warmUpDevServer(baseURL);
// Check SuperSync server health ONCE here, before workers start.
// Without this, each worker checks independently on startup — with many workers
// running simultaneously, the concurrent health-check requests can overload the
// supersync server and cause false negatives, making workers skip all their tests.
// By storing the result in an env var set before workers are forked, every worker
// reads the cached result instantly instead of making HTTP requests.
const healthy = await isServerHealthy().catch(() => false);
process.env.SUPERSYNC_SERVER_HEALTHY = healthy ? 'true' : 'false';
if (healthy) {
console.log('SuperSync server healthy — supersync tests will run');
} else {
console.log('SuperSync server not available — supersync tests will be skipped');
}
};
export default globalSetup;

View file

@ -191,8 +191,14 @@ export class SuperSyncPage extends BasePage {
// piggybacked ops in the response, causing data to sync between clients
// outside of explicit syncAndWait(). Set here (not in createSimulatedClient)
// so tests using enableWebSocket:true still get immediate uploads.
// 3. Block WsTriggeredDownloadService: Even when routeWebSocket() closes the
// connection, a WS notification can slip through the moment the connection
// opens (before it's closed). Setting __SP_E2E_BLOCK_WS_DOWNLOAD ensures
// WsTriggeredDownloadService ignores any such notifications, preventing
// uncontrolled background syncs that race with explicit syncAndWait() calls.
await this.page.evaluate(() => {
(globalThis as any).__SP_E2E_BLOCK_IMMEDIATE_UPLOAD = true;
(globalThis as any).__SP_E2E_BLOCK_WS_DOWNLOAD = true;
});
}

View file

@ -672,8 +672,11 @@ test.describe('@supersync SuperSync E2E', () => {
await saveAndGoHomeBtn.click();
console.log('[Archive Test] Client B clicked Save and go home (archiving)');
// Wait for navigation back to work view
await clientB.page.waitForURL(/tag\/TODAY/, { timeout: 10000 });
// Wait for navigation back to work view.
// Use negative lookahead to avoid matching /tag/TODAY/daily-summary prematurely.
await clientB.page.waitForURL(/(tag\/TODAY(?!\/daily-summary))/, {
timeout: 10000,
});
await clientB.page.waitForLoadState('networkidle');
console.log('[Archive Test] Client B back on work view after archiving');

View file

@ -239,8 +239,34 @@ export const createSimulatedClient = async (
}
});
// Navigate to app and wait for ready
await page.goto('/');
// Navigate to app with retry for transient ERR_CONNECTION_REFUSED.
// Under parallel load (many workers × 2-3 browser contexts each), the Angular
// dev server can temporarily refuse connections. Retrying recovers from this
// without failing the test outright.
let lastGotoError: Error | null = null;
for (let attempt = 0; attempt < 3; attempt++) {
try {
await page.goto('/');
lastGotoError = null;
break;
} catch (e) {
lastGotoError = e as Error;
const isConnectionRefused = lastGotoError.message.includes(
'ERR_CONNECTION_REFUSED',
);
if (attempt < 2 && isConnectionRefused) {
const delay = 1000 * (attempt + 1);
console.log(
`[Client ${clientName}] page.goto('/') failed (attempt ${attempt + 1}/3): ERR_CONNECTION_REFUSED — retrying in ${delay}ms`,
);
await new Promise((r) => setTimeout(r, delay));
} else {
break; // Non-connection error or last attempt — let it throw below
}
}
}
if (lastGotoError) throw lastGotoError;
await waitForAppReady(page);
const workView = new WorkViewPage(page, `${clientName}-${testPrefix}`);

View file

@ -0,0 +1,57 @@
import { join } from 'path';
import { readdir, unlink } from 'fs/promises';
import { log, warn } from 'electron-log/main';
/**
* Deletes stale LevelDB LOCK files left in the IndexedDB directory.
*
* When Electron (Chromium) exits uncleanly (e.g., session logout with autostart),
* the LevelDB LOCK files inside the IndexedDB backing stores are sometimes not
* cleaned up. On the next launch these orphaned files block IndexedDB from opening,
* producing the "Internal error opening backing store for indexedDB.open" error.
*
* This is safe to call because by the time it runs, `requestSingleInstanceLock()`
* has already ensured we are the only running instance, so no legitimate process
* can hold those locks.
*
* Only runs on Linux, where this startup race condition has been observed.
*
* @see https://github.com/electron/electron/issues/18263
* @see https://github.com/super-productivity/super-productivity/issues/7191
*/
export const clearStaleLevelDbLocks = async (userDataPath: string): Promise<void> => {
if (process.platform !== 'linux') {
return;
}
const idbDir = join(userDataPath, 'IndexedDB');
let entries: string[];
try {
entries = await readdir(idbDir);
} catch (e: unknown) {
// Directory doesn't exist yet (fresh install) — nothing to clean up.
// Any other error (e.g., EACCES, EPERM) is unexpected and worth logging.
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
warn(`[clearStaleLevelDbLocks] Could not read IndexedDB directory:`, e);
}
return;
}
const leveldbDirs = entries.filter((e) => e.endsWith('.leveldb'));
await Promise.all(
leveldbDirs.map(async (dir) => {
const lockPath = join(idbDir, dir, 'LOCK');
try {
await unlink(lockPath);
log(`[clearStaleLevelDbLocks] Removed stale lock: ${lockPath}`);
} catch (e: unknown) {
// LOCK file doesn't exist or is already released — this is the normal case
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
warn(`[clearStaleLevelDbLocks] Could not remove ${lockPath}:`, e);
}
}
}),
);
};

View file

@ -48,7 +48,7 @@ export interface ElectronAPI {
fileSyncRemove(args: { filePath: string }): Promise<unknown | Error>;
fileSyncListFiles(args: { dirPath: string }): Promise<string[] | Error>; // NEW
fileSyncListFiles(args: { dirPath: string }): Promise<string[] | Error>;
checkDirExists(args: { dirPath: string }): Promise<true | Error>;

View file

@ -112,6 +112,36 @@ export const initLocalFileSyncAdapter = (): void => {
return true;
} catch (e) {
log('ERR: error while checking dir ' + dirPath);
if ((e as NodeJS.ErrnoException).code === 'EACCES') {
log(
'ERR: Permission denied. If running as a snap, ensure the "home" or "removable-media" interface is connected.',
);
}
error(e);
return e instanceof Error ? e : new Error(String(e));
}
},
);
ipcMain.handle(
IPC.FILE_SYNC_LIST_FILES,
(
ev,
{
dirPath,
}: {
dirPath: string;
},
): string[] | Error => {
try {
return readdirSync(dirPath);
} catch (e) {
log('ERR: Sync error while listing files in ' + dirPath);
if ((e as NodeJS.ErrnoException).code === 'EACCES') {
log(
'ERR: Permission denied. If running as a snap, ensure the "home" or "removable-media" interface is connected.',
);
}
error(e);
return e instanceof Error ? e : new Error(String(e));
}

View file

@ -459,7 +459,7 @@ const appCloseHandler = (app: App): void => {
mainWin.on('close', (event) => {
// NOTE: this might not work if we run a second instance of the app
log('close, isQuiting:', getIsQuiting());
log('close event: isQuiting=', getIsQuiting(), 'pendingBeforeCloseIds=', ids);
if (!getIsQuiting()) {
if (getIsMinimizeToTray()) {
const indicator = ensureIndicator();

View file

@ -68,8 +68,8 @@ const ea: ElectronAPI = {
dataStr: string | undefined;
}>,
fileSyncRemove: (filePath) => _invoke('FILE_SYNC_REMOVE', filePath) as Promise<void>,
fileSyncListFiles: ({ dirPath }) =>
_invoke('FILE_SYNC_LIST_FILES', dirPath) as Promise<string[] | Error>,
fileSyncListFiles: (args) =>
_invoke('FILE_SYNC_LIST_FILES', args) as Promise<string[] | Error>,
checkDirExists: (dirPath) =>
_invoke('CHECK_DIR_EXISTS', dirPath) as Promise<true | Error>,

View file

@ -43,7 +43,7 @@ export enum IPC {
FILE_SYNC_LOAD = 'FILE_SYNC_LOAD',
FILE_SYNC_SAVE = 'FILE_SYNC_SAVE',
FILE_SYNC_REMOVE = 'FILE_SYNC_REMOVE',
FILE_SYNC_LIST_FILES = 'FILE_SYNC_LIST_FILES', // NEW
FILE_SYNC_LIST_FILES = 'FILE_SYNC_LIST_FILES',
FILE_SYNC_GET_REV_AND_CLIENT_UPDATE = 'FILE_SYNC_GET_REV_AND_CLIENT_UPDATE',
CHECK_DIR_EXISTS = 'CHECK_DIR_EXISTS',

View file

@ -13,14 +13,15 @@ import { CONFIG } from './CONFIG';
import { lazySetInterval } from './shared-with-frontend/lazy-set-interval';
import { initIndicator } from './indicator';
import { quitApp, showOrFocus } from './various-shared';
import { createWindow } from './main-window';
import { createWindow, getWin } from './main-window';
import { IdleTimeHandler } from './idle-time-handler';
import { destroyTaskWidget } from './task-widget/task-widget';
import {
initializeProtocolHandling,
processPendingProtocolUrls,
} from './protocol-handler';
import { getIsQuiting, setIsLocked } from './shared-state';
import { getIsQuiting, setIsQuiting, setIsLocked } from './shared-state';
import { clearStaleLevelDbLocks } from './clear-stale-idb-locks';
import * as fs from 'fs';
const ICONS_FOLDER = __dirname + '/assets/icons/';
@ -356,18 +357,32 @@ export const startApp = (): void => {
appIN.on('will-quit', () => {
// un-register all shortcuts.
globalShortcut.unregisterAll();
// Safe to remove IPC listeners here: all windows are closed and before-close
// IPC flows (sync, finish-day) are guaranteed to have completed.
ipcMain.removeAllListeners();
});
appIN.on('before-quit', () => {
log('App before-quit: cleaning up resources');
// Clean up task widget before quitting
appIN.on('before-quit', (event) => {
log('App before-quit: isQuiting=', getIsQuiting());
if (!getIsQuiting()) {
// Native quit path (Cmd+Q, Dock > Quit on macOS): app.quit() was called
// by the OS without going through quitApp(), so isQuiting was never set.
// Prevent the immediate quit and delegate to the window close handler,
// which manages the before-close callback flow (sync, finish-day, etc.)
// and sets isQuiting=true before re-quitting.
event.preventDefault();
const win = getWin();
if (win && !win.isDestroyed()) {
win.close();
} else {
// No window to close — set flag and re-trigger quit directly.
setIsQuiting(true);
app.quit();
}
return;
}
// isQuiting=true: all before-close IPC work is complete — safe to clean up.
destroyTaskWidget();
// Remove all IPC listeners to prevent memory leaks
ipcMain.removeAllListeners();
// Clear any pending timeouts/intervals
if (global.gc) {
global.gc();
}
@ -425,6 +440,10 @@ export const startApp = (): void => {
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
async function createMainWin(): Promise<void> {
// Remove stale LevelDB LOCK files before the renderer opens IndexedDB.
// Orphaned locks from unclean session shutdowns block the backing store open.
await clearStaleLevelDbLocks(app.getPath('userData'));
mainWin = await createWindow({
app,
IS_DEV,

View file

@ -1,7 +1,7 @@
{
"CFG": {
"REPO": "Repository (owner/repo)",
"TOKEN": "Personal Access Token (optional, for private repos)",
"TOKEN": "Personal Access Token (required for private repos/optional for public repos)",
"FILTER_USERNAME": "Filter username (ignore own comments for update detection)",
"BACKLOG_QUERY": "Backlog query (default: \"sort:updated state:open assignee:@me\")",
"HOW_TO_GET_TOKEN": "How to get a token"

View file

@ -1,6 +1,7 @@
<nav
class="mobile-bottom-nav"
[class.entrance]="isEntrance()"
(animationend)="onEntranceAnimationEnd($event)"
>
<button
mat-button
@ -25,7 +26,7 @@
mat-fab
class="add-task-button"
[class.smaller]="hasProjectBacklog()"
[class.hide-for-native]="isAndroid && isEntrance()"
[class.hide-for-native]="isEntranceAnimating()"
color="primary"
(click)="showAddTaskBar()"
>

View file

@ -105,7 +105,7 @@
height: 28px;
}
// On Android, hide during entrance animation (native add-task button is visible).
// On Android, hide during the slide-up entrance animation.
// var(--transition-standard) on .add-task-button already transitions opacity,
// so removing the class fades in automatically.
&.hide-for-native {

View file

@ -1,4 +1,12 @@
import { ChangeDetectionStrategy, Component, inject, input, output } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
effect,
inject,
input,
output,
signal,
} from '@angular/core';
import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
@ -48,6 +56,35 @@ export class MobileBottomNavComponent {
isEntrance = input(false);
readonly isAndroid = IS_ANDROID_NATIVE;
// Tracks whether the slide-up entrance animation is actively running.
// On Android, the web FAB is hidden only during this animation (not the full
// entrance duration) so that the button appears as soon as the nav settles —
// even if the native startup overlay was not shown (e.g. process-death recreation).
readonly isEntranceAnimating = signal(false);
private _entranceAnimationTimeout: ReturnType<typeof setTimeout> | undefined;
constructor() {
effect((onCleanup) => {
if (this.isEntrance() && this.isAndroid) {
this.isEntranceAnimating.set(true);
// Safety fallback: clear if animationend doesn't fire
// (e.g. prefers-reduced-motion: reduce → animation: none).
// Timeout = 250ms delay + 400ms duration + 50ms buffer = 700ms
this._entranceAnimationTimeout = setTimeout(() => {
this.isEntranceAnimating.set(false);
}, 700);
onCleanup(() => clearTimeout(this._entranceAnimationTimeout));
}
});
}
onEntranceAnimationEnd(event: AnimationEvent): void {
if (event.animationName === 'slide-up-from-bottom') {
this.isEntranceAnimating.set(false);
clearTimeout(this._entranceAnimationTimeout);
}
}
readonly T = T;
readonly TODAY_TAG = TODAY_TAG;
readonly todayTagId = TODAY_TAG.id;

View file

@ -475,6 +475,9 @@ export class GlobalThemeService {
* Adds/removes CSS classes when keyboard shows/hides.
*/
private _initIOSKeyboardHandling(): void {
// Show the native iOS accessory bar ("Done" button) above the keyboard
Keyboard.setAccessoryBarVisible({ isVisible: true });
Keyboard.addListener('keyboardWillShow', (info: KeyboardInfo) => {
Log.log('iOS keyboard will show', info);
this.document.body.classList.add(BodyClass.isKeyboardVisible);

View file

@ -207,6 +207,18 @@
<mat-icon>done_all</mat-icon>
</button>
}
<!-- Reset cycles button (In Progress state, Pomodoro only) -->
@if (isShowCompleteSessionButton() && isPomodoro()) {
<button
mat-icon-button
[matTooltip]="T.F.FOCUS_MODE.RESET_CYCLES | translate"
(click)="resetCycles()"
class="reset-cycles-btn"
>
<mat-icon>restart_alt</mat-icon>
</button>
}
</div>
</div>

View file

@ -24,6 +24,7 @@ import {
completeTask,
focusModeLoaded,
pauseFocusSession,
resetCycles,
selectFocusTask,
setFocusModeMode,
setFocusSessionDuration,
@ -181,6 +182,7 @@ export class FocusModeMainComponent {
isShowTimeAdjustButtons = computed(
() => this._isInProgress() && this.mode() !== FocusModeMode.Flowtime,
);
isPomodoro = computed(() => this.mode() === FocusModeMode.Pomodoro);
// Play button should be disabled when sync with tracking is enabled but no task is selected
isPlayButtonDisabled = computed(() => {
@ -402,6 +404,10 @@ export class FocusModeMainComponent {
this._store.dispatch(unPauseFocusSession());
}
resetCycles(): void {
this._store.dispatch(resetCycles());
}
selectMode(mode: FocusModeMode | string | number): void {
if (!Object.values(FocusModeMode).includes(mode as FocusModeMode)) {
return;

View file

@ -10,7 +10,6 @@ import {
IssueProviderState,
} from '../issue.model';
import { ICAL_TYPE } from '../issue.const';
import { IssueLog } from '../../../core/log';
export const selectIssueProviderState = createFeatureSelector<IssueProviderState>(
ISSUE_PROVIDER_FEATURE_KEY,
@ -46,10 +45,11 @@ export const selectIssueProviderById = <T extends IssueProvider>(
createSelector(selectIssueProviderState, ({ entities }) => {
const issueProvider = entities[id];
if (!issueProvider) {
// Do not include the entity in the error — IssueLog history is exportable
// and providers may carry credentials.
throw new Error(`No issueProvider found for id ${id}`);
}
if (issueProviderKey && issueProvider.issueProviderKey !== issueProviderKey) {
IssueLog.log(issueProviderKey, issueProvider);
throw new Error(
`IssueProvider found for id ${id} is not of type ${issueProviderKey} but ${issueProvider.issueProviderKey}`,
);

View file

@ -56,7 +56,8 @@
.title {
flex-shrink: 1;
min-width: 0;
@include truncateText();
overflow: hidden;
word-break: break-word;
font-size: var(--planner-font-size-mobile);
@include mq(xs) {

View file

@ -40,7 +40,8 @@
font-size: var(--planner-font-size-mobile);
flex-shrink: 1;
min-width: 0;
@include truncateText();
overflow: hidden;
word-break: break-word;
@include mq(xs) {
font-size: var(--planner-font-size);

View file

@ -40,7 +40,8 @@
font-size: var(--planner-font-size-mobile);
flex-shrink: 1;
min-width: 0;
@include truncateText();
overflow: hidden;
word-break: break-word;
@include mq(xs) {
font-size: var(--planner-font-size);

View file

@ -105,7 +105,8 @@ done-toggle {
line-height: 1.5;
position: relative;
@include truncateText();
overflow: hidden;
word-break: break-word;
flex-grow: 1;
flex-shrink: 1;
@ -119,7 +120,8 @@ done-toggle {
padding-right: var(--s-half);
padding-top: var(--s-half);
padding-bottom: var(--s-half);
@include truncateText();
overflow: hidden;
word-break: break-word;
line-height: 1.1;
font-size: var(--planner-font-size-smaller-mobile);

View file

@ -281,9 +281,9 @@ describe('AddTaskBarParserService', () => {
});
describe('Parsing Integration', () => {
it('should call updateEstimate when parsing text', async () => {
it('should call updateEstimate when text contains estimate syntax', async () => {
await service.parseAndUpdateText(
'Task with potential time estimate',
'Task with estimate 30m',
mockConfig,
mockProjects,
mockTags,
@ -293,7 +293,7 @@ describe('AddTaskBarParserService', () => {
expect(mockStateService.updateEstimate).toHaveBeenCalled();
});
it('should handle null time estimates', async () => {
it('should not call updateEstimate for text without estimate on first parse', async () => {
await service.parseAndUpdateText(
'Simple task',
mockConfig,
@ -302,7 +302,24 @@ describe('AddTaskBarParserService', () => {
mockDefaultProject,
);
expect(mockStateService.updateEstimate).toHaveBeenCalledWith(null);
expect(mockStateService.updateEstimate).not.toHaveBeenCalled();
});
it('should not call updateEstimate when typing text without estimate syntax for the first time', async () => {
// Simulates: user sets estimate via dropdown, then types task title.
// The parser has no previous result (first parse after empty input).
// Since the parsed estimate is null and there's no previous result to
// diff against, updateEstimate should NOT be called to avoid wiping
// out the dropdown-set value.
await service.parseAndUpdateText(
'My new task',
mockConfig,
mockProjects,
mockTags,
mockDefaultProject,
);
expect(mockStateService.updateEstimate).not.toHaveBeenCalled();
});
it('should call updateSpent when parsing text', async () => {

View file

@ -188,8 +188,9 @@ export class AddTaskBarParserService {
}
if (
!this._previousParseResult ||
this._previousParseResult.timeEstimate !== currentResult.timeEstimate
(!this._previousParseResult && currentResult.timeEstimate !== null) ||
(this._previousParseResult &&
this._previousParseResult.timeEstimate !== currentResult.timeEstimate)
) {
this._stateService.updateEstimate(currentResult.timeEstimate);
}

View file

@ -17,7 +17,7 @@ import {
import { GlobalConfigService } from '../../config/global-config.service';
import { unique } from '../../../util/unique';
import { TaskService } from '../task.service';
import { from, of } from 'rxjs';
import { EMPTY, from, of } from 'rxjs';
import { ProjectService } from '../../project/project.service';
import { TagService } from '../../tag/tag.service';
import { shortSyntax } from '../short-syntax';
@ -102,6 +102,12 @@ export class ShortSyntaxEffects {
),
),
mergeMap(([{ task, originalAction }, tags, projects, defaultProjectId]) => {
// Guard: task may have been archived/deleted while the effect was in flight
// (e.g., a concurrent sync applied a moveToArchive op). Skip processing
// to prevent "Cannot read properties of undefined" errors.
if (!task) {
return EMPTY;
}
const isReplaceTagIds = originalAction.type === TaskSharedActions.updateTask.type;
return from(
shortSyntax(

View file

@ -16,6 +16,7 @@ import {
pauseFocusSession,
showFocusOverlay,
startFocusSession,
tick,
unPauseFocusSession,
} from '../../focus-mode/store/focus-mode.actions';
import { IPC } from '../../../../../electron/shared-with-frontend/ipc-events.const';
@ -109,6 +110,9 @@ export class TaskElectronEffects {
pauseFocusSession,
unPauseFocusSession,
completeFocusSession,
// Keep tray time in sync during focus-mode breaks and focus sessions
// without an active task (addTimeSpent is gated on currentTask.id).
tick,
),
withLatestFrom(

View file

@ -104,6 +104,29 @@ describe('Task Reducer', () => {
expect(state.entities['task1']!.subTaskIds).toContain('subTask3');
expect(state.entities['subTask3']).toEqual({ ...newSubTask, parentId: 'task1' });
});
it('should roll up the parent time estimate when adding a subtask with an estimate', () => {
const parent = createTask('parent');
const subTask = createTask('subTask', { timeEstimate: 2.5 * 60 * 60 * 1000 });
const state: TaskState = {
...initialTaskState,
ids: ['parent'],
entities: {
parent,
},
};
const result = taskReducer(
state,
fromActions.addSubTask({
task: subTask,
parentId: 'parent',
}),
);
expect(result.entities['parent']!.subTaskIds).toEqual(['subTask']);
expect(result.entities['parent']!.timeEstimate).toBe(2.5 * 60 * 60 * 1000);
});
});
describe('moveSubTask (anchor-based)', () => {

View file

@ -519,7 +519,7 @@ export const taskReducer = createReducer<TaskState>(
state,
);
return {
const stateWithParentTask: TaskState = {
...stateCopy,
// update current task to new sub task if parent was current before
...(state.currentTaskId === parentId ? { currentTaskId: task.id } : {}),
@ -532,6 +532,8 @@ export const taskReducer = createReducer<TaskState>(
},
},
};
return reCalcTimesForParentIfParent(parentId, stateWithParentTask);
}),
on(toggleStart, (state) => {

View file

@ -55,6 +55,7 @@ import { DialogScheduleTaskComponent } from '../../planner/dialog-schedule-task/
import { DialogDeadlineComponent } from '../dialog-deadline/dialog-deadline.component';
import { Store } from '@ngrx/store';
import { selectIssueProviderById } from '../../issue/store/issue-provider.selectors';
import { IssueLog } from '../../../core/log';
import { TaskTitleComponent } from '../../../ui/task-title/task-title.component';
import { MatIcon } from '@angular/material/icon';
import { TaskListComponent } from '../task-list/task-list.component';
@ -365,9 +366,17 @@ export class TaskDetailPanelComponent implements OnInit, AfterViewInit, OnDestro
distinctUntilChanged(),
switchMap((issueProviderId) =>
issueProviderId
? this._store.select(
selectIssueProviderById<IssueProviderJira>(issueProviderId, 'JIRA'),
)
? this._store
.select(
selectIssueProviderById<IssueProviderJira>(issueProviderId, 'JIRA'),
)
.pipe(
// Orphan issueProviderId — see #7135.
catchError((err: unknown) => {
IssueLog.warn('Jira header setup skipped', err);
return of(null);
}),
)
: of(null),
),
takeUntilDestroyed(this._destroyRef),

View file

@ -53,7 +53,10 @@ export class DialogGetAndEnterAuthCodeComponent implements OnDestroy {
T: typeof T = T;
token?: string;
// Always use manual code entry flow (show input field)
// Always use manual code entry flow (show input field on all platforms).
// The automatic deep-link redirect flow was reverted: it requires the
// redirect URI to be registered in the Dropbox developer console, and
// Android may kill the app during auth, losing the in-memory code verifier.
readonly isNativePlatform = false;
private _authCodeSub?: Subscription;

View file

@ -28,6 +28,8 @@ import { loadAllData } from '../../root-store/meta/load-all-data.action';
import { bulkApplyHydrationOperations } from '../apply/bulk-hydration.action';
import { CLIENT_ID_PROVIDER, ClientIdProvider } from '../util/client-id.provider';
import { MAX_VECTOR_CLOCK_SIZE } from '@sp/shared-schema';
import { IndexedDBOpenError } from '../core/errors/indexed-db-open.error';
import { IDB_OPEN_ERROR_RELOAD_KEY } from './operation-log-hydrator.service';
describe('OperationLogHydratorService', () => {
let service: OperationLogHydratorService;
@ -1319,4 +1321,65 @@ describe('OperationLogHydratorService', () => {
);
});
});
describe('IndexedDB open error handling', () => {
let reloadSpy: jasmine.Spy;
beforeEach(() => {
sessionStorage.removeItem(IDB_OPEN_ERROR_RELOAD_KEY);
if (!jasmine.isSpy(window.alert)) {
spyOn(window, 'alert');
}
(window.alert as jasmine.Spy).calls.reset();
reloadSpy = spyOn(service as any, '_triggerReload');
});
afterEach(() => {
sessionStorage.removeItem(IDB_OPEN_ERROR_RELOAD_KEY);
});
it('should silently auto-reload for backing store error on first occurrence', async () => {
const err = new IndexedDBOpenError(
new Error('Internal error opening backing store for indexedDB.open'),
);
mockRecoveryService.recoverPendingRemoteOps.and.rejectWith(err);
await expectAsync(service.hydrateStore()).toBeRejected();
expect(reloadSpy).toHaveBeenCalledTimes(1);
expect(sessionStorage.getItem(IDB_OPEN_ERROR_RELOAD_KEY)).toBe('1');
// No dialog on first attempt — autostart users aren't watching
expect(window.alert).not.toHaveBeenCalled();
});
it('should show recovery dialog and NOT auto-reload when already reloaded once', async () => {
sessionStorage.setItem(IDB_OPEN_ERROR_RELOAD_KEY, '1');
const err = new IndexedDBOpenError(
new Error('Internal error opening backing store for indexedDB.open'),
);
mockRecoveryService.recoverPendingRemoteOps.and.rejectWith(err);
await expectAsync(service.hydrateStore()).toBeRejected();
expect(reloadSpy).not.toHaveBeenCalled();
expect(window.alert).toHaveBeenCalledTimes(1);
});
it('should NOT auto-reload for non-backing-store IDB open errors', async () => {
const err = new IndexedDBOpenError(new Error('QuotaExceededError'));
mockRecoveryService.recoverPendingRemoteOps.and.rejectWith(err);
await expectAsync(service.hydrateStore()).toBeRejected();
expect(reloadSpy).not.toHaveBeenCalled();
});
it('should clear the reload key after successful hydration', async () => {
sessionStorage.setItem(IDB_OPEN_ERROR_RELOAD_KEY, '1');
// Successful hydration — no errors thrown
await service.hydrateStore();
expect(sessionStorage.getItem(IDB_OPEN_ERROR_RELOAD_KEY)).toBeNull();
});
});
});

View file

@ -27,6 +27,13 @@ import { MAX_CONFLICT_RETRY_ATTEMPTS } from '../core/operation-log.const';
import { AppDataComplete } from '../model/model-config';
import { CLIENT_ID_PROVIDER, ClientIdProvider } from '../util/client-id.provider';
import { limitVectorClockSize } from '../../core/util/vector-clock';
import { IS_ELECTRON } from '../../app.constants';
/**
* sessionStorage key used to track auto-reload attempts after IndexedDB backing store errors.
* Exported for use in tests.
*/
export const IDB_OPEN_ERROR_RELOAD_KEY = 'sp_idb_open_reload_attempt';
/**
* Handles the hydration (loading) of the application state from the operation log
@ -117,6 +124,7 @@ export class OperationLogHydratorService {
'OperationLogHydratorService: Snapshot is invalid/corrupted. Attempting recovery...',
);
await this.recoveryService.attemptRecovery();
sessionStorage.removeItem(IDB_OPEN_ERROR_RELOAD_KEY);
return;
}
@ -280,6 +288,7 @@ export class OperationLogHydratorService {
OpLog.normal(
'OperationLogHydratorService: Fresh install detected. No data to load.',
);
sessionStorage.removeItem(IDB_OPEN_ERROR_RELOAD_KEY);
return;
}
@ -362,6 +371,11 @@ export class OperationLogHydratorService {
// Retry any failed remote ops from previous conflict resolution attempts
// Now that state is fully hydrated, dependencies might be resolved
await this.retryFailedRemoteOps();
// Clear the auto-reload guard so that a fresh backing-store error in the same
// tab session gets the auto-reload treatment again rather than going straight
// to the manual recovery dialog.
sessionStorage.removeItem(IDB_OPEN_ERROR_RELOAD_KEY);
} catch (e) {
OpLog.err('OperationLogHydratorService: Error during hydration', e);
@ -658,6 +672,32 @@ export class OperationLogHydratorService {
? error.originalError.message
: String(error.originalError);
// Hoist platform detection — used in both branches below to avoid computing twice
const isFlatpak = IS_ELECTRON && window.ea?.isFlatpak?.();
const isSnap = !isFlatpak && IS_ELECTRON && window.ea?.isSnap?.();
// For backing-store errors (common during Linux session startup with autostart),
// auto-reload once after the user dismisses the dialog. By the time the dialog
// is dismissed the OS / Flatpak sandbox will usually have finished initializing.
// A sessionStorage counter prevents an infinite reload loop on genuine errors.
if (error.isBackingStoreError) {
const reloadCount = +(sessionStorage.getItem(IDB_OPEN_ERROR_RELOAD_KEY) || '0');
if (reloadCount === 0) {
// Silent auto-reload on first occurrence — most likely a transient startup
// timing issue (Flatpak sandbox not ready, stale LOCK file). The user is
// typically not watching (autostart scenario), so a blocking dialog requiring
// a click before the reload is unnecessary friction. If the reload fixes it,
// the user never needs to know. If it fails again, the dialog below runs.
OpLog.warn(
'IndexedDB backing-store error on first attempt — triggering silent auto-reload',
);
sessionStorage.setItem(IDB_OPEN_ERROR_RELOAD_KEY, '1');
this._triggerReload();
return;
}
}
// Second failure, or non-backing-store error: show full manual recovery instructions.
let message =
'Database Error - Cannot Load Data\n\n' +
'Super Productivity cannot open its database. ' +
@ -671,7 +711,11 @@ export class OperationLogHydratorService {
'Recovery steps:\n' +
'1. Close ALL browser tabs and windows\n' +
'2. Restart the app\n' +
'3. If using Linux Snap, try: snap set core experimental.refresh-app-awareness=true\n' +
(isFlatpak
? '3. If using Linux Flatpak with autostart, try disabling autostart and launching manually\n'
: isSnap
? '3. If using Linux Snap, try: snap set core experimental.refresh-app-awareness=true\n'
: '3. If using Linux with autostart, try disabling autostart and launching manually\n') +
'4. If issue persists, check available disk space\n\n';
}
@ -682,4 +726,16 @@ export class OperationLogHydratorService {
alertDialog(message);
}
/**
* Triggers an app reload. Uses Electron IPC in Electron context, browser reload otherwise.
* Extracted as a method to allow spying in unit tests.
*/
private _triggerReload(): void {
if (IS_ELECTRON) {
window.ea.reloadMainWin();
} else {
window.location.reload();
}
}
}

View file

@ -317,11 +317,12 @@ export class Dropbox implements SyncProviderServiceInterface<SyncProviderId.Drop
/**
* Gets the OAuth redirect URI.
*
* Always returns null to use Dropbox's manual code entry flow.
* User must copy the code from Dropbox's page and paste it manually.
* This works reliably across all platforms (web, Electron, Android, iOS).
*
* @returns null for manual code entry flow
* Returns null on all platforms to use Dropbox's manual code entry flow.
* User copies the code from Dropbox's page and pastes it manually.
* This is the most reliable cross-platform approach the automatic deep-link
* redirect flow was reverted because the Dropbox developer console redirect URI
* registration is uncertain and Android may kill the app during auth, losing
* the in-memory code verifier.
*/
private _getRedirectUri(): string | null {
return null;

View file

@ -3,5 +3,5 @@ export interface FileAdapter {
writeFile(filePath: string, dataStr: string): Promise<void>;
deleteFile(filePath: string): Promise<void>;
checkDirExists?(dirPath: string): Promise<boolean>;
listFiles?(dirPath: string): Promise<string[]>; // NEW
listFiles?(dirPath: string): Promise<string[]>;
}

View file

@ -29,7 +29,7 @@ export const isOperationSyncCapable = (
/**
* Type guard to check if a provider uses file-based operation sync.
* File-based providers (WebDAV, Dropbox, LocalFile) use file storage for sync.
* File-based providers (WebDAV, Dropbox, LocalFile, Nextcloud) use file storage for sync.
*/
export const isFileBasedProvider = (
provider: SyncProviderBase<SyncProviderId>,

View file

@ -18,6 +18,13 @@
padding-top: var(--mac-title-bar-padding);
}
// On iOS, the virtual keyboard overlaps position:fixed elements since the CDK
// overlay container does not shrink with Capacitor's resize:'native' mode.
// Subtract the keyboard height so Save/Close buttons stay above the keyboard.
:host-context(body.isNativeMobile.isKeyboardVisible) {
height: calc(100% - var(--keyboard-height, 0px));
}
.formatting-toolbar {
display: flex;
flex-wrap: nowrap;

View file

@ -15,7 +15,7 @@ const base64UrlEncode = (buffer: Uint8Array): string => {
* Uses crypto.subtle when available, otherwise falls back to hash-wasm.
*/
const sha256 = async (data: Uint8Array): Promise<ArrayBuffer> => {
if (typeof crypto !== 'undefined' && crypto.subtle !== undefined) {
if (typeof crypto !== 'undefined' && crypto.subtle != null) {
return crypto.subtle.digest('SHA-256', data as BufferSource);
}

View file

@ -40,7 +40,7 @@
"DONATE_PAGE": {
"BUTTON_TEXT": "Wesprzyj przez GitHub Sponsors",
"INTRO_1": "Super Productivity jest finansowany w całości przez społeczność. Nie ma śledzenia, reklam ani zbierania danych — Twoje zadania pozostają na Twoim urządzeniu.",
"INTRO_2": "Jeśli cenisz takie podejście i chcesz, aby projekt był zdrowy i się rozwijał, dobrowolna darowizna jest bardzo mile widziana."
"INTRO_2": "Jeśli cenisz takie podejście i chcesz, aby projekt był utrzymywany w dobrej kondycji i się rozwijał, dobrowolna darowizna jest bardzo mile widziana."
},
"EXAMPLE_TASKS": {
"LEARN_KEYBOARD_SHORTCUTS": {
@ -167,8 +167,18 @@
"EVENT_STR": "Wydarzenie",
"EVENTS_STR": "Wydarzenia"
},
"CONTEXT_MENU": {
"CREATE_TASK": "Utwórz jako zadanie",
"OPEN_IN_CALENDAR": "Otwórz w kalendarzu",
"RESCHEDULE": "Reschedule",
"DELETE_EVENT": "Usuń zdarzenie"
},
"S": {
"CAL_PROVIDER_ERROR": "Błąd dostawcy kalendarza: {{errTxt}}"
"CAL_PROVIDER_ERROR": "Błąd dostawcy kalendarza: {{errTxt}}",
"EVENT_HIDDEN": "Calendar event hidden",
"EVENT_RESCHEDULED": "Event rescheduled",
"EVENT_DELETED": "Event deleted",
"TIME_BLOCK_ERROR": "Failed to sync time block: {{errTxt}}"
}
},
"CLIPBOARD_IMAGE": {
@ -265,7 +275,7 @@
},
"GITEA": {
"FORM": {
"HOST": "Host (np.: https://try.gitea.io)",
"HOST": "Host (np. https://try.gitea.io)",
"REPO_FULL_NAME": "Nazwa użytkownika lub nazwa organizacji/projektu",
"REPO_FULL_NAME_DESCRIPTION": "Można go znaleźć jako część adresu URL podczas przeglądania projektu w przeglądarce.",
"SCOPE": "Zakres",
@ -422,7 +432,15 @@
"ADVANCED_CONFIG": "Zaawansowana konfiguracja",
"CONNECTED": "Połączone",
"DELETE_CONFIRM": "Czy na pewno chcesz usunąć tego dostawcę zgłoszeń? Usunięcie oznacza, że <strong>wszystkie wcześniej zaimportowane zadania zgłoszeń stracą swoje powiązanie</strong>. Tej operacji nie można cofnąć!",
"DISCONNECT": "Rozłączone"
"DISCONNECT": "Rozłączone",
"EDIT_TITLE": "Edit {{title}} Config",
"LOAD_OPTIONS": "Load Options",
"LOAD_OPTIONS_FAILED": "Failed to load options",
"LOADING_OPTIONS": "Loading options...",
"OPTIONS_LOADED": "Options loaded",
"RELOAD_OPTIONS": "Reload Options",
"SETUP_TITLE": "Setup {{title}} Config",
"TEST_CONNECTION": "Test Connection"
}
},
"ISSUE_PANEL": {
@ -494,7 +512,7 @@
},
"FORM_CRED": {
"ALLOW_SELF_SIGNED": "Zezwalaj na certyfikat self-signed",
"HOST": "Host (np.: http://mój-host.pl:1234)",
"HOST": "Host (np. http://moja-domena.pl:1234)",
"PASSWORD": "Token / hasło",
"USE_PAT": "Zamiast tego użyj osobistego tokena dostępu (LEGACY)",
"USER_NAME": "E-mail / nazwa użytkownika",
@ -692,13 +710,13 @@
"STRIKETHROUGH": "Przekreślenie",
"TASK_LIST": "Lista zadań"
},
"VIEW_PARSED": "Wyświetlanie jako przeanalizowany (nieedytowalny) markdown",
"VIEW_SPLIT": "Wyświetlanie przeanalizowanego i nieprzeanalizowanego markdown w widoku podzielonym",
"VIEW_TEXT_ONLY": "Wyświetlanie jako nieprzeanalizowany tekst"
"VIEW_PARSED": "Wyświetl jako sparsowany (nieedytowalny) markdown",
"VIEW_SPLIT": "Wyświetl jako sparsowany i niesformatowany Markdown w widoku podzielonym",
"VIEW_TEXT_ONLY": "Wyświetl jako niesparsowany tekst"
},
"NOTE_CMP": {
"DISABLE_PARSE": "Wyłącz markdown parsing",
"ENABLE_PARSE": "Włącz markdown parse"
"DISABLE_PARSE": "Wyłącz parsowanie Markdown przy podglądzie",
"ENABLE_PARSE": "Włącz parsowanie Markdown"
},
"NOTES_CMP": {
"ADD_BTN": "Dodaj nową notatkę",
@ -931,7 +949,7 @@
},
"END": "Koniec pracy",
"INSERT_BEFORE": "Przed",
"LUNCH_BREAK": "Przerwa obiadowa",
"LUNCH_BREAK": "Przerwa na lunch",
"MONTH": "Miesiąc",
"NO_TASKS": "Obecnie nie ma żadnych zadań. Dodaj zadania za pomocą przycisku plus (+) na górnym pasku.",
"NOW": "Teraz",
@ -951,16 +969,16 @@
"OK": "Zrób to!"
},
"D_EDIT": {
"CURRENT_STREAK": "Aktualna passa",
"CURRENT_STREAK": "Obecna seria",
"DAILY_GOAL": "Cel dzienny",
"DAYS": "Dni",
"L_COUNTER": "Licznik"
"L_COUNTER": "Stan"
},
"FORM": {
"ADD_NEW": "Dodaj prosty licznik",
"HELP": "Tutaj możesz skonfigurować proste przyciski, które pojawią się w prawym górnym rogu. Mogą to być timery albo zwykły licznik, który zwiększa się po kliknięciu.",
"L_COUNTDOWN_DURATION": "Czas trwania odliczania",
"L_DAILY_GOAL": "Codzienny cel dla udanej passy",
"L_DAILY_GOAL": "Dzienny cel utrzymania serii",
"L_ICON": "Ikona",
"L_ICON_ON": "Ikona po włączeniu",
"L_IS_ENABLED": "Włączone",
@ -971,11 +989,11 @@
"L_TITLE": "Tytuł",
"L_TRACK_STREAKS": "Śledź serie",
"L_TYPE": "Typ",
"L_WEEKDAYS": "Dni powszednie, aby sprawdzić smugę",
"L_WEEKLY_FREQUENCY": "Liczba wykonań na tydzień",
"L_WEEKDAYS": "Dni sprawdzania serii",
"L_WEEKLY_FREQUENCY": "Wymagana liczba ukończeń w tygodniu",
"TITLE": "Proste liczniki",
"TYPE_CLICK_COUNTER": "Licznik kliknięć",
"TYPE_REPEATED_COUNTDOWN": "Powtarzane odliczanie",
"TYPE_REPEATED_COUNTDOWN": "Codzienne odliczanie",
"TYPE_STOPWATCH": "Stoper"
},
"HABIT_TRACKER": {
@ -987,7 +1005,7 @@
},
"S": {
"GOAL_REACHED_1": "Osiągnąłeś swój dzisiejszy cel!",
"GOAL_REACHED_2": "Aktualny czas trwania passy:"
"GOAL_REACHED_2": "Czas trwania obecnej serii:"
}
},
"SYNC": {
@ -1537,7 +1555,7 @@
"QA_NEXT_WEEK": "Następny tydzień",
"QA_TODAY": "Dzisiaj",
"QA_TOMORROW": "Jutro",
"REMIND_AT": "Przypomnij o",
"REMIND_AT": "Przypomnij",
"REMOVE_DEADLINE": "Usuń termin",
"RO_1H": "1 godzinę przed",
"RO_5M": "5 minut przed",
@ -1577,7 +1595,7 @@
"QA_NEXT_WEEK": "Harmonogram na następny tydzień",
"QA_TODAY": "Harmonogram na dziś",
"QA_TOMORROW": "Harmonogram na jutro",
"REMIND_AT": "Przypomnij o",
"REMIND_AT": "Przypomnij",
"RO_1H": "1 godzinę przed rozpoczęciem",
"RO_5M": "5 minut przed rozpoczęciem",
"RO_10M": "10 minut przed rozpoczęciem",
@ -1594,7 +1612,7 @@
"TITLE": "Wybierz datę i godzinę"
},
"D_TIME": {
"ADD_FOR_OTHER_DAY": "Dodaj czas spędzony dla innego dnia",
"ADD_FOR_OTHER_DAY": "Dodaj czas spędzony innego dnia",
"DELETE_FOR": "Usuń wpis dla dnia",
"ESTIMATE": "Szacowanie",
"TIME_SPENT": "Czas spędzony",
@ -1604,7 +1622,7 @@
"D_TIME_FOR_DAY": {
"ADD_ENTRY_FOR": "Dodaj nowy wpis dla {{date}}",
"DATE": "Data nowego wpisu",
"HELP": "Przykłady:<br> 30m =&gt; 30 minut<br> 2h =&gt; 2 godziny<br> 2h30m =&gt; 2 godziny i 30 minut<br> 1.5h =&gt; 1 godzina i 30 minut",
"HELP": "Przykłady:<br> 30m =&gt; 30 minut<br> 2h =&gt; 2 godziny<br> 2h30m =&gt; 2 godziny i 30 minut<br> 1.5h =&gt; 1 godzina i 30 minut<br> 03:15 => 3 godziny i 15 minut",
"TINE_SPENT": "Czas spędzony",
"TITLE": "Dodaj dla dnia"
},
@ -1928,7 +1946,7 @@
"DO_IT": "Zrób to!",
"DONT_SHOW_AGAIN": "Nie pokazuj ponownie",
"DUPLICATE": "Powiel",
"DURATION_DESCRIPTION": "np. \"5h 23m\", oznacza 5 godzin i 23 minuty",
"DURATION_DESCRIPTION": "np. \"5h 23m\" oznacza 5 godzin i 23 minuty",
"EDIT": "Edytuj",
"ENABLED": "Włączone",
"EXAMPLE_VAL": "np. 32m",
@ -1970,7 +1988,7 @@
"HELP": "Włącz lub wyłącz konkretne funkcje aplikacji w całym interfejsie użytkownika.",
"ISSUES_PANEL": "Panel zgłoszeń",
"PLANNER": "Planer",
"PROJECT_NOTES": "Notatki projektowe",
"PROJECT_NOTES": "Notatki projektu",
"SCHEDULE": "Harmonogram",
"SCHEDULE_DAY_PANEL": "Panel dnia harmonogramu",
"SEARCH": "Szukaj",
@ -1979,7 +1997,9 @@
"TITLE": "Funkcje aplikacji",
"USER_PROFILES": "Profile użytkowników (eksperymentalne)",
"USER_PROFILES_HINT": "Pozwala tworzyć i przełączać się między różnymi profilami użytkowników, z których każdy ma oddzielne ustawienia, zadania i konfiguracje synchronizacji. Przycisk zarządzania profilem pojawi się w prawym górnym rogu po włączeniu. Uwaga: Wyłączenie tej funkcji ukryje interfejs użytkownika, ale zachowa dane profilu (Funkcja eksperymentalna, brak gwarancji. Upewnij się, że masz kopię zapasową danych!).",
"USER_PROFILES_WARNING": "<strong>Funkcja eksperymentalna:</strong> Ta funkcja jest wciąż w fazie rozwoju i może zostać usunięta lub znacząco zmieniona w przyszłej aktualizacji. Używasz na własne ryzyko. Upewnij się, że masz kopię zapasową danych."
"USER_PROFILES_WARNING": "<strong>Funkcja eksperymentalna:</strong> Ta funkcja jest wciąż w fazie rozwoju i może zostać usunięta lub znacząco zmieniona w przyszłej aktualizacji. Używasz na własne ryzyko. Upewnij się, że masz kopię zapasową danych.",
"EXPERIMENTAL_WARNING_TITLE": "Funkcja eksperymentalna",
"EXPERIMENTAL_WARNING_MSG": "Ta funkcja jest wciąż w fazie rozwoju i może zostać usunięta lub znacząco zmieniona w przyszłej aktualizacji. Używasz na własne ryzyko. Upewnij się, że masz kopię zapasową danych.<br/><br/>Czy na pewno chcesz ją aktywować?"
},
"AUTO_BACKUPS": {
"HELP": "Automatycznie zapisuj wszystkie dane w folderze aplikacji, aby mieć kopię na wypadek problemów.",
@ -2215,8 +2235,8 @@
"HELP": "Funkcja harmonogramu ma dać Ci lepszy podgląd tego, jak zaplanowane zadania układają się w czasie. Ustawienia znajdziesz w sekcji „Harmonogram”.",
"L_IS_LUNCH_BREAK_ENABLED": "Włącz przerwę na lunch",
"L_IS_WORK_START_END_ENABLED": "Ogranicz przepływ niezaplanowanych zadań do określonych godzin pracy",
"L_LUNCH_BREAK_END": "Koniec przerwy obiadowej",
"L_LUNCH_BREAK_START": "Początek przerwy obiadowej",
"L_LUNCH_BREAK_END": "Koniec przerwy na lunch",
"L_LUNCH_BREAK_START": "Początek przerwy na lunch",
"L_WORK_END": "Koniec dnia pracy",
"L_WORK_START": "Początek dnia pracy",
"LUNCH_BREAK_START_END_DESCRIPTION": "np. 13:00",
@ -2362,14 +2382,14 @@
"HABITS": "Nawyki",
"HELP": "Pomoc",
"HM": {
"CALENDARS": "Jak to zrobić: Połącz kalendarze",
"CONTRIBUTE": "Wkład",
"CALENDARS": "Poradnik: Łączenie się z kalendarzem",
"CONTRIBUTE": "Współtwórz",
"GET_HELP_ONLINE": "Uzyskaj pomoc online",
"KEYBOARD": "Howto: Zaawansowana klawiatura",
"KEYBOARD": "Poradnik: Zaawansowana klawiatura",
"REDDIT_COMMUNITY": "Społeczność Reddit",
"REPORT_A_PROBLEM": "Zgłoś problem",
"START_WELCOME": "Rozpocznij wycieczkę powitalną",
"SYNC": "Jak to zrobić: Konfiguracja synchronizacji"
"START_WELCOME": "Rozpocznij samouczek powitalny",
"SYNC": "Poradnik: Konfiguracja synchronizacji"
},
"METRICS": "Metryki",
"NO_PROJECT_INFO": "Brak dostępnych projektów. Możesz utworzyć nowy projekt, klikając przycisk 'Utwórz projekt'.",
@ -2393,9 +2413,9 @@
"TAGS": "Tagi",
"TASK_LIST": "Lista zadań",
"TASKS": "Zadania",
"TOGGLE_SHOW_BOOKMARKS": "Pokaż/ukryj zakładki",
"TOGGLE_SHOW_BOOKMARKS": "Pokaż/Ukryj zakładki",
"TOGGLE_SHOW_ISSUE_PANEL": "Pokaż/Ukryj panel zgłoszeń",
"TOGGLE_SHOW_NOTES": "Pokaż/Ukryj Notatki Projektu",
"TOGGLE_SHOW_NOTES": "Pokaż/Ukryj notatki projektu",
"TOGGLE_TRACK_TIME": "Start/Stop zliczania czasu",
"TRIGGER_SYNC": "Ręczne wyzwalanie synchronizacji",
"UNPLAN_ALL_TASKS": "Odplanowano wszystkie zadania",
@ -2679,7 +2699,7 @@
"NO_PLANNED_TASK_ALL_DONE": "wszystkie zadania zakończone",
"NO_PLANNED_TASKS": "Nie zaplanowano zadań",
"RECURRING_TASKS": "Zadania cykliczne",
"RESET_BREAK_TIMER": "Zresetuj bez czasu przerwy",
"RESET_BREAK_TIMER": "Zresetuj czas bez przerwy",
"TIME_ESTIMATED": "Zaplanowany czas:",
"TODAY_REMAINING": "Dziś pozostało:",
"WITHOUT_BREAK": "Bez przerwy:",

View file

@ -34,7 +34,7 @@
"BY": "după",
"NO_RESULTS": "Nicio imagine găsită. Te rog încearcă un alt termen de căutare.",
"SEARCH_LABEL": "Caută imagini",
"SEARCH_PLACEHOLDER": "ex: natură, munți, abstract",
"SEARCH_PLACEHOLDER": "d.e. natură, munți, abstract",
"TITLE": "Alege o imagine de fundal din Unsplash"
},
"DONATE_PAGE": {
@ -94,8 +94,8 @@
"ADD_NEW_PANEL": "Adaugă un nou panou",
"BACKLOG_TASK_FILTER_ALL": "Toate",
"BACKLOG_TASK_FILTER_NO_BACKLOG": "Exclude",
"BACKLOG_TASK_FILTER_ONLY_BACKLOG": "Doar restante",
"BACKLOG_TASK_FILTER_TYPE": "Sarcini restante",
"BACKLOG_TASK_FILTER_ONLY_BACKLOG": "Doar backlog",
"BACKLOG_TASK_FILTER_TYPE": "Sarcini din backlog",
"COLUMNS": "Coloane",
"TAGS_EXCLUDED": "Etichete excluse",
"TAGS_REQUIRED": "Etichete obligatorii",
@ -167,8 +167,18 @@
"EVENT_STR": "eveniment",
"EVENTS_STR": "evenimente"
},
"CONTEXT_MENU": {
"CREATE_TASK": "Creează ca sarcină",
"OPEN_IN_CALENDAR": "Deschide în calendar",
"RESCHEDULE": "Replanifică",
"DELETE_EVENT": "Șterge evenimentul"
},
"S": {
"CAL_PROVIDER_ERROR": "Eroare furnizor calendar: {{errTxt}}"
"CAL_PROVIDER_ERROR": "Eroare furnizor calendar: {{errTxt}}",
"EVENT_HIDDEN": "Eveniment calendar ascuns",
"EVENT_RESCHEDULED": "Eveniment reprogramat",
"EVENT_DELETED": "Eveniment șters",
"TIME_BLOCK_ERROR": "Nu s-a putut sincroniza blocul de timp: {{errTxt}}"
}
},
"CLIPBOARD_IMAGE": {
@ -265,7 +275,7 @@
},
"GITEA": {
"FORM": {
"HOST": "Gazdă (ex: https://try.gitea.io)",
"HOST": "Gazdă (d.e. https://try.gitea.io)",
"REPO_FULL_NAME": "Nume utilizator sau nume organizație/proiect",
"REPO_FULL_NAME_DESCRIPTION": "Poate fi găsit ca parte a URL-ului cînd vizualizezi proiectul într-un navigator web.",
"SCOPE": "Domeniu de aplicare",
@ -303,7 +313,7 @@
"FILTER_USER": "Filtrează după numele de utilizator",
"GITLAB_BASE_URL": "URL de bază personalizat GitLab (opțional)",
"PROJECT": "nume utilizator/proiect",
"PROJECT_HINT": "ex: super-productivity/super-productivity",
"PROJECT_HINT": "d.e. super-productivity/super-productivity",
"SCOPE": "Domeniu de aplicare",
"SCOPE_ALL": "Toate",
"SCOPE_ASSIGNED": "Atribuite mie",
@ -421,8 +431,16 @@
"DIALOG": {
"ADVANCED_CONFIG": "Configurare avansată",
"CONNECTED": "Conectat",
"DELETE_CONFIRM": "Dorești să ștergi acest furnizor de probleme? Ștergerea înseamnă că <strong>toate sarcinile de probleme importate anterior vor pierde referința</strong>. Aceasta nu poate fi anulată!",
"DISCONNECT": "Deconectează"
"DELETE_CONFIRM": "Dorești să ștergi acest furnizor de probleme? Ștergerea înseamnă că <strong>toate sarcinile de probleme importate anterior vor pierde referința</strong>. Aceasta acțiune nu poate fi anulată!",
"DISCONNECT": "Deconectează",
"EDIT_TITLE": "Editează configurația {{title}}",
"LOAD_OPTIONS": "Încarcă opțiunile",
"LOAD_OPTIONS_FAILED": "Nu s-au putut încărca opțiunile",
"LOADING_OPTIONS": "Se încarcă opțiunile...",
"OPTIONS_LOADED": "Opțiuni încărcate",
"RELOAD_OPTIONS": "Reîncarcă opțiunile",
"SETUP_TITLE": "Editează configurația {{title}}",
"TEST_CONNECTION": "Testează conexiunea"
}
},
"ISSUE_PANEL": {
@ -432,37 +450,37 @@
},
"JIRA": {
"BANNER": {
"BLOCK_ACCESS_MSG": "Jira: To prevent API lock out, access has been blocked by Super Productivity. You probably should check your Jira settings!",
"BLOCK_ACCESS_UNBLOCK": "Unblock"
"BLOCK_ACCESS_MSG": "Jira: Pentru a preveni blocarea API-ului, accesul a fost blocat de Super Productivity. Probabil ar trebui să verifici setările Jira!",
"BLOCK_ACCESS_UNBLOCK": "Deblochează"
},
"CFG_CMP": {
"ALWAYS_ASK": "Always open dialog",
"DO_NOT": "Don't transition",
"DONE": "Status for completing task",
"ENABLE": "Enable Jira integration",
"ENABLE_TRANSITIONS": "Enable Transition Handling",
"IN_PROGRESS": "Status for starting task",
"LOAD_SUGGESTIONS": "Load Suggestions",
"MAP_CUSTOM_FIELDS": "Load Story Points",
"MAP_CUSTOM_FIELDS_INFO": "Unfortunately some of Jira's data is saved under custom fields which are different for every installation. To include this data, you need to select the proper custom field for it. Currently there is only the story points field that needs to be mapped.",
"OPEN": "Status for pausing task",
"SELECT_ISSUE_FOR_TRANSITIONS": "Select issue to load available transitions",
"STORY_POINTS": "Story Points Field Name",
"TRANSITION": "Transition Handling"
"ALWAYS_ASK": "Deschide mereu dialogul",
"DO_NOT": "Nu tranziționa",
"DONE": "Stare pentru finalizarea sarcinii",
"ENABLE": "Activează integrarea Jira",
"ENABLE_TRANSITIONS": "Activează gestionarea tranzițiilor",
"IN_PROGRESS": "Stare pentru începerea sarcinii",
"LOAD_SUGGESTIONS": "Încarcă sugestiile",
"MAP_CUSTOM_FIELDS": "Încarcă Story Points",
"MAP_CUSTOM_FIELDS_INFO": "Din păcate unele din datele Jira sînt salvate sub cîmpuri particulare (custom fields) care sînt diferite pentru fiecare instalare. Pentru a include aceste date, trebuie să selectezi cîmpul personalizat adecvat. Momentan, există doar cîmpul pentru Story Points care trebuie asociat.",
"OPEN": "Stare pentru punerea pe pauză a sarcinii",
"SELECT_ISSUE_FOR_TRANSITIONS": "Selectează problema pentru a încărca tranzițiile disponibile",
"STORY_POINTS": "Nume cîmp Story Points",
"TRANSITION": "Gestionarea tranzițiilor"
},
"DIALOG_CONFIRM_ASSIGNMENT": {
"MSG": "<strong>{{summary}}</strong> is currently assigned to <strong>{{assignee}}</strong>. Do you want to assign it to yourself?",
"OK": "Do it!"
"MSG": "<strong>{{summary}}</strong> Este asociat curent cu <strong>{{assignee}}</strong>. Dorești să îl asociezi pentru tine?",
"OK": "Fă-o!"
},
"DIALOG_INITIAL": {
"TITLE": "Setup Jira for Project"
"TITLE": "Configurează Jira pentru proiect"
},
"DIALOG_TRANSITION": {
"CHOOSE_STATUS": "Choose status to assign",
"CURRENT_ASSIGNEE": "Current Assignee:",
"CURRENT_STATUS": "Current Status:",
"TASK_NAME": "Task name:",
"TITLE": "Jira: Update Status"
"CHOOSE_STATUS": "Alege starea de atribuit",
"CURRENT_ASSIGNEE": "Atribuit curent:",
"CURRENT_STATUS": "Starea curentă:",
"TASK_NAME": "Nume sarcină:",
"TITLE": "Jira: Actualizează starea"
},
"DIALOG_WORKLOG": {
"CHECKBOXES": {
@ -929,32 +947,32 @@
"TEXT": "<p>Orarul îți oferă o viziune de ansamblu despre cum se desfășoară sarcinile planificate în timp. Este generat automat din sarcinile tale și necesită doar <strong>estimări de timp</strong> pentru a funcționa.</p><p>Două lucruri sînt distinse: <strong>Sarcini programate</strong>, care sînt afișate la ora planificată și <strong>Sarcini obișnuite</strong>, care ar trebui să curgă în jurul acestor evenimente fixe.</p><p>Dacă setezi o oră de început și sfîrșit a lucrului (recomandat), sarcinile obișnuite nu vor fi niciodată plasate în afara acestor limite.</p>",
"TITLE": "Orar"
},
"END": "Work End",
"INSERT_BEFORE": "Before",
"LUNCH_BREAK": "Lunch Break",
"MONTH": "Month",
"NO_TASKS": "Currently there are no tasks. Please add some tasks via the plus (+) button in the top bar.",
"NOW": "Now",
"PLAN_END_DAY": "End of {{date}}",
"PLAN_START_DAY": "Start of {{date}}",
"SHIFT_KEY_INFO": "Hold Shift to toggle day planning mode",
"START": "Work Start"
"END": "Finalul muncii",
"INSERT_BEFORE": "Înainte",
"LUNCH_BREAK": "Pauză de masă",
"MONTH": "Lună",
"NO_TASKS": "Momentan nu există sarcini. Adaugă cîteva sarcini prin butonul plus (+) din bara de sus.",
"NOW": "Acum",
"PLAN_END_DAY": "Final de {{date}}",
"PLAN_START_DAY": "Început de {{date}}",
"SHIFT_KEY_INFO": "Ține apăsat Shift pentru a comuta modul de planificare zilnică",
"START": "Începerea muncii"
},
"SEARCH_BAR": {
"INFO": "Start typing to search for tasks",
"NO_RESULTS": "No tasks found matching your search",
"PLACEHOLDER": "Search for task or task notes"
"INFO": "Începe să tastezi pentru a căuta sarcini",
"NO_RESULTS": "Nu s-au găsit sarcini care să se potrivească căutării",
"PLACEHOLDER": "Caută sarcină sau notițe despre sarcină"
},
"SIMPLE_COUNTER": {
"D_CONFIRM_REMOVE": {
"MSG": "Deleting a simple counter, will also delete all past data tracked on it. Are you sure you want to proceed?",
"OK": "Do it!"
"MSG": "Ștergerea unui contor simplu va șterge și toate datele urmărite pe el. Ești sigur că vrei să continui?",
"OK": "Fă-o!"
},
"D_EDIT": {
"CURRENT_STREAK": "Current Streak",
"DAILY_GOAL": "Daily Goal",
"DAYS": "Days",
"L_COUNTER": "Count"
"CURRENT_STREAK": "Serie curentă",
"DAILY_GOAL": "Obiectiv zilnic",
"DAYS": "Zile",
"L_COUNTER": "Număr"
},
"FORM": {
"ADD_NEW": "Adaugă un contor simplu sau un obicei",
@ -967,7 +985,7 @@
"L_IS_HIDE_BUTTON": "Ascunde butonul din bara de unelte",
"L_STREAK_MODE": "Mod urmărire serie",
"L_STREAK_MODE_SPECIFIC_DAYS": "Zilele săptămînii",
"L_STREAK_MODE_WEEKLY_FREQUENCY": "Frecvență săptămînală (ex: de 3 ori pe săptămînă)",
"L_STREAK_MODE_WEEKLY_FREQUENCY": "Frecvență săptămînală (d.e. de 3 ori pe săptămînă)",
"L_TITLE": "Titlu",
"L_TRACK_STREAKS": "Urmărește seriile",
"L_TYPE": "Tip",
@ -1000,40 +1018,40 @@
"FORCE_UPLOAD": "Forțarea încărcării datelor poate duce la pierderea acestora. Ești sigur că vrei să continui?"
},
"D_AUTH_CODE": {
"FOLLOW_LINK": "Please open the following link and copy the auth code provided there into the input field below.",
"FOLLOW_LINK_MOBILE": "Please tap the button below to authorize. You will be automatically redirected back to the app.",
"GET_AUTH_CODE": "Get Authorization Code",
"L_AUTH_CODE": "Enter Auth Code",
"TITLE": "Login: {{provider}}",
"WAITING_FOR_AUTH": "Waiting for authentication..."
"FOLLOW_LINK": "Te rog deschide următoarea legătură și copiază codul de autorizare furnizat acolo în cîmpul de intrare de mai jos.",
"FOLLOW_LINK_MOBILE": "Te rog apasă butonul de mai jos pentru a autoriza. Vei fi redirecționat automat înapoi la aplicație.",
"GET_AUTH_CODE": "Obține codul de autorizare",
"L_AUTH_CODE": "Întrodu codul de autorizare",
"TITLE": "Autentificare: {{provider}}",
"WAITING_FOR_AUTH": "Se așteaptă autentificarea..."
},
"D_CONFLICT": {
"ADDITIONAL_INFO": "Additional Info",
"CHANGES": "Changes",
"CHANGES_SINCE_LAST_SYNC": "Changes since last sync",
"COMPARISON_RESULT": "Comparison Result",
"DATE": "Date",
"LAST_SYNC": "Last Sync:",
"LAST_SYNCED": "Last Synced",
"LAST_WRITE": "Last Write",
"ADDITIONAL_INFO": "Informații suplimentare",
"CHANGES": "Modificări",
"CHANGES_SINCE_LAST_SYNC": "Modificări de la ultima sincronizare",
"COMPARISON_RESULT": "Rezultatul comparării",
"DATE": "Dată",
"LAST_SYNC": "Ultima Sincronizare:",
"LAST_SYNCED": "Ultima sincronizare",
"LAST_WRITE": "Ultima scriere",
"LOCAL": "Local",
"LOCAL_REMOTE": "Local -> Remote",
"NEVER": "Never",
"OVERWRITE_WARNING": "WARNING: The {{targetName}} data contains approximately {{targetChanges}} changes while the {{sourceName}} data only has {{sourceChanges}} changes. Are you sure you want to overwrite the {{targetName}} data with the {{sourceName}} version? This action cannot be undone.",
"REMOTE": "Remote",
"RESULT": "Result",
"TEXT": "<p>Update from remote. Both local and remote data seem to be modified.</p>",
"TIME": "Time",
"TIMESTAMP": "Timestamp",
"TITLE": "Sync: Conflicting Data",
"USE_LOCAL": "Keep local",
"USE_REMOTE": "Keep remote",
"VECTOR_CLOCK": "Vector Clock",
"VECTOR_CLOCK_HEADING": "Vector Clock",
"VECTOR_COMPARISON_CONCURRENT": "Concurrent (True Conflict)",
"VECTOR_COMPARISON_EQUAL": "Equal",
"VECTOR_COMPARISON_LOCAL_GREATER": "Local > Remote",
"VECTOR_COMPARISON_LOCAL_LESS": "Local < Remote"
"LOCAL_REMOTE": "Local -> Distant",
"NEVER": "Niciodată",
"OVERWRITE_WARNING": "ATENȚIE: Datele {{targetName}} conțin aproximativ {{targetChanges}} modificări în timp ce datele {{sourceName}} au doar {{sourceChanges}} modificări. Ești sigur că vrei să înlocuiești datele {{targetName}} cu versiunea {{sourceName}}? Această acțiune nu poate fi anulată.",
"REMOTE": "Distant",
"RESULT": "Rezultat",
"TEXT": "<p>Actualizează de la distanță. Atît datele locale cît și cele de pe server par modificate.</p>",
"TIME": "Timp",
"TIMESTAMP": "Marca temporală",
"TITLE": "Sincronizare: Date conflictuale",
"USE_LOCAL": "Păstrează local",
"USE_REMOTE": "Păstrează distant",
"VECTOR_CLOCK": "Ceas vectorial",
"VECTOR_CLOCK_HEADING": "Ceas vectorial",
"VECTOR_COMPARISON_CONCURRENT": "Concurent (conflict adevărat)",
"VECTOR_COMPARISON_EQUAL": "Egal",
"VECTOR_COMPARISON_LOCAL_GREATER": "Local > Distant",
"VECTOR_COMPARISON_LOCAL_LESS": "Local < Distant"
},
"D_DATA_REPAIR_CONFIRM": {
"MSG": "Datele tale par deteriorate. Dorești să încerci repararea automată? Aceasta poate duce la pierderea minoră de date.",
@ -1045,28 +1063,28 @@
"UNKNOWN_ERROR": "Eroare necunoscută de sincronizare: {{err}}"
},
"D_DECRYPT_ERROR": {
"BTN_OVER_WRITE_REMOTE": "Use This Password & Overwrite Remote",
"CHANGE_PW_AND_DECRYPT": "Retry Decrypt",
"P1": "Your remote data is encrypted and decryption failed.",
"P2_FORGOT_PW": "If you <strong>forgot your password</strong>, enter the correct password and click \"Retry Decrypt\".",
"P3_PW_CHANGED": "If you <strong>changed your password on another device</strong>, enter your new password and click \"Overwrite Remote\". This will upload your local data to the server.",
"PASSWORD": "Password",
"TITLE": "Decryption Failed"
"BTN_OVER_WRITE_REMOTE": "Folosește această parolă și suprascrie-o pe cea de la distanță",
"CHANGE_PW_AND_DECRYPT": "Reîncearcă decriptarea",
"P1": "Datele tale de la distanță sînt criptate și decriptarea a eșuat.",
"P2_FORGOT_PW": "Dacă ai <strong>uitat parola</strong>, întrodu parola corectă și apasă \"Reîncearcă decriptarea\".",
"P3_PW_CHANGED": "Dacă ai <strong>schimbat parola pe un alt dispozitiv</strong>, întrodu noua parolă și apasă \"Suprascrie datele de la distanță\". Aceasta va încărca datele locale criptate cu această parolă pe server.",
"PASSWORD": "Parolă",
"TITLE": "Decriptarea a eșuat"
},
"D_ENTER_PASSWORD": {
"BTN_FORCE_OVERWRITE": "Use Local Data",
"BTN_SAVE_AND_SYNC": "Save & Sync",
"FORCE_OVERWRITE_CONFIRM": "This will <strong>delete all data on the server</strong> and upload your local data encrypted with this password. Continue?",
"FORCE_OVERWRITE_INFO": "If you prefer to keep your local data, you can overwrite the server instead.",
"FORCE_OVERWRITE_TITLE": "Overwrite Server Data?",
"MESSAGE": "The sync server contains encrypted data, but no encryption password is configured on this device. Please enter your encryption password to decrypt and sync your data.",
"PASSWORD_LABEL": "Encryption Password",
"TITLE": "Encryption Password Required"
"BTN_FORCE_OVERWRITE": "Folosește datele locale",
"BTN_SAVE_AND_SYNC": "Salvează și sincronizează",
"FORCE_OVERWRITE_CONFIRM": "Acest lucru va <strong>șterge toate datele de pe server</strong> și va încărca datele locale criptate cu această parolă. Continui?",
"FORCE_OVERWRITE_INFO": "Dacă preferi să păstrezi datele locale, poți suprascrie serverul în schimb.",
"FORCE_OVERWRITE_TITLE": "Suprascrii datele de pe server?",
"MESSAGE": "Serverul de sincronizare conține date criptate, dar nu este configurată nicio parolă de criptare pe acest dispozitiv. Te rog să întroduci parola de criptare pentru a decripta și sincroniza datele.",
"PASSWORD_LABEL": "Parolă de criptare",
"TITLE": "Parolă de criptare necesară"
},
"D_FRESH_CLIENT_CONFIRM": {
"MESSAGE": "This appears to be a fresh installation. Remote data with {{count}} changes was found. Do you want to download and overwrite your local data with it?",
"OK": "Download Remote Data",
"TITLE": "Initial Sync"
"MESSAGE": "Aceasta pare să fie o instalare curată. Datele de la distanță cu {{count}} modificări au fost găsite. Dorești să descarci și să înlocuiești datele locale cu ele?",
"OK": "Descarcă datele de la distanță",
"TITLE": "Sincronizare inițială"
},
"D_INCOMPLETE_SYNC": {
"BTN_CLOSE_APP": "Închide aplicația",
@ -1271,13 +1289,17 @@
"SETUP_ENCRYPTION_PASSWORD_WARNING": "Folosește aceeași parolă pe toate dispozitivele. O parolă uitată poate fi resetată prin suprascrierea datelor de la distanță.",
"SETUP_ENCRYPTION_TITLE": "SuperSync: Setare parolă"
},
"NEXTCLOUD": {
"L_SERVER_URL": "Nextcloud Server URL",
"SERVER_URL_DESCRIPTION": "e.g. https://cloud.example.com or https://example.com/nextcloud",
"L_APP_PASSWORD": "App Password",
"APP_PASSWORD_DESCRIPTION": "Go to Nextcloud → Settings → Security → App passwords to generate one"
},
"TITLE": "Sync",
"WEB_DAV": {
"CONDITIONAL_HEADER_WARNING_MSG": "Your WebDAV server does not support conditional headers (If-Unmodified-Since). This means Super Productivity cannot detect concurrent changes from other devices during upload.<br><br>Sync will still work, but there is a small risk of data conflicts if you make changes on multiple devices simultaneously.",
"CONDITIONAL_HEADER_WARNING_TITLE": "Reduced Sync Safety",
"CORS_INFO": "<strong>Making it work in a web browser:</strong> Allow Super Productivity to make CORS requests to your WebDAV server. This can have negative security implications! For Nextcloud, please refer to <a href='https://github.com/nextcloud/server/issues/3131'>this GitHub thread</a> for more information. One approach to make this work on mobile is allowing \"https://app.super-productivity.com\" via the Nextcloud app <a href='https://apps.nextcloud.com/apps/webapppassword'>webapppassword<a>. Use at your own risk!</p>",
"CORS_INFO": "<strong>Making it work in a web browser:</strong> You need to allow Super Productivity to make CORS requests to your server. For Nextcloud, one approach is allowing \"https://app.super-productivity.com\" via the <a href='https://apps.nextcloud.com/apps/webapppassword'>webapppassword</a> app. Refer to <a href='https://github.com/nextcloud/server/issues/3131'>this GitHub thread</a> for more information.",
"D_SYNC_FOLDER_PATH": "Path relative to the WebDAV server root where sync files will be stored (e.g. '/super-productivity' or '/'). This is NOT your server's internal directory path.",
"INFO": "<strong>Warning:</strong> WebDAV implementations differ wildly. Super Productivity is known to work well with Nextcloud, <strong>but it may not work with your provider</strong>. Test thoroughly before relying on it.",
"INFO": "<strong>Warning:</strong> Generic WebDAV sync is provided <strong>as-is without support</strong>. If you are using Nextcloud, please use the Nextcloud sync option instead.",
"L_BASE_URL": "Base URL",
"L_PASSWORD": "Password",
"L_SYNC_FOLDER_PATH": "Sync Folder Path",
@ -1315,6 +1337,8 @@
"ERROR_PERMISSION": "File access denied. Please check your filesystem permissions.",
"ERROR_PERMISSION_FLATPAK": "File access denied. Grant filesystem permission via Flatseal or use a path inside ~/.var/app/",
"ERROR_PERMISSION_SNAP": "File access denied. Run 'snap connect super-productivity:home' or use a path inside ~/snap/super-productivity/common/",
"ERROR_REMOTE_FILE_EMPTY": "Remote sync file is empty. This can happen after an interrupted sync. Overwrite remote with your local data?",
"ERROR_REMOTE_FILE_LOCKED": "Remote sync file is locked by the server. This may resolve on the next sync attempt.",
"ERROR_UNABLE_TO_READ_REMOTE_DATA": "Error while syncing. Unable to read remote data. Maybe you enabled encryption and your local password does not match the one used to encrypt the remote data?",
"FINISH_DAY_SYNC_ERROR": "Sync error while finishing day. Please try again.",
"FRESH_CLIENT_SYNC_CANCELLED": "Sincronizare inițială anulată. Datele de la distanță nu au fost aplicate.",
@ -1405,11 +1429,11 @@
"TITLE": "Basic Settings"
},
"S": {
"ARCHIVE_CLEANUP_FAILED": "Curățarea datelor arhivate pentru eticheta ștearsă a eșuat",
"UPDATED": "Tag settings were updated"
"ARCHIVE_CLEANUP_FAILED": "A eșuat curățarea datelor arhivate pentru eticheta ștearsă",
"UPDATED": "Setările etichetei au fost actualizate"
},
"TTL": {
"ADD_NEW_TAG": "Add new Tag"
"ADD_NEW_TAG": "Adaugă etichetă nouă"
}
},
"TAG_FOLDER": {
@ -1489,47 +1513,47 @@
"ADD_SUB_TASK": "Adaugă subsarcină",
"ADD_TO_MY_DAY": "Adaugă la lista de azi",
"ADD_TO_PROJECT": "Adaugă la proiect",
"CONVERT_TO_PARENT_TASK": "Convert to parent task",
"DELETE": "Delete task",
"DELETE_REPEAT_INSTANCE": "Delete recurring task instance",
"DROP_ATTACHMENT": "Drop here to attach to \"{{title}}\"",
"DUPLICATE": "Duplicate",
"CONVERT_TO_PARENT_TASK": "Convertește în sarcină părinte",
"DELETE": "Șterge sarcina",
"DELETE_REPEAT_INSTANCE": "Șterge instanța de sarcină recurentă",
"DROP_ATTACHMENT": "Plasează aici pentru a atașa la \"{{title}}\"",
"DUPLICATE": "Duplică",
"EDIT_DEADLINE": "Editează termen limită",
"EDIT_SCHEDULED": "Reschedule",
"FOCUS_SESSION": "Start focus session",
"MARK_DONE": "Mark as completed",
"MOVE_TO_BACKLOG": "Move to backlog",
"MOVE_TO_OTHER_PROJECT": "Move to project",
"MOVE_TO_REGULAR": "Move to regular list",
"MOVE_TO_TOP": "Move to top",
"OPEN_ATTACH": "Attach file or link",
"OPEN_ISSUE": "Open in browser",
"OPEN_TIME": "Time tracking",
"EDIT_SCHEDULED": "Planifică din nou",
"FOCUS_SESSION": "Începe sesiune de concentrare",
"MARK_DONE": "Marchează ca finalizată",
"MOVE_TO_BACKLOG": "Mută în backlog",
"MOVE_TO_OTHER_PROJECT": "Mută în proiect",
"MOVE_TO_REGULAR": "Mută în lista obișnuită",
"MOVE_TO_TOP": "Mută în partea de sus",
"OPEN_ATTACH": "Atașează fișier sau legătură",
"OPEN_ISSUE": "Deschide în navigatorul web",
"OPEN_TIME": "Contorizare timp",
"REMOVE_DEADLINE": "Șterge termen limită",
"REMOVE_FROM_MY_DAY": "Elimină din lista de azi",
"SCHEDULE": "Schedule task",
"SCHEDULE": "Planifică sarcina",
"SET_DEADLINE": "Setează termen limită",
"TOGGLE_ATTACHMENTS": "Show/hide attachments",
"TOGGLE_DETAIL_PANEL": "Show/hide additional info",
"TOGGLE_DONE": "Toggle completion status",
"TOGGLE_SUB_TASK_VISIBILITY": "Toggle subtask visibility",
"TOGGLE_TAGS": "Toggle tags",
"TRACK_TIME": "Start tracking time",
"TRACK_TIME_STOP": "Pause tracking time",
"UNSCHEDULE_TASK": "Unschedule task",
"UPDATE_ISSUE_DATA": "Update issue data"
"TOGGLE_ATTACHMENTS": "Arată/Ascunde atașamente",
"TOGGLE_DETAIL_PANEL": "Arată/Ascunde informații suplimentare",
"TOGGLE_DONE": "Comută starea de finalizare",
"TOGGLE_SUB_TASK_VISIBILITY": "Comută vizibilitatea subsarcinilor",
"TOGGLE_TAGS": "Comută etichetele",
"TRACK_TIME": "Începe contorizarea timpului",
"TRACK_TIME_STOP": "Pune pe pauză contorizarea timpului",
"UNSCHEDULE_TASK": "Șterge planificarea sarcinii",
"UPDATE_ISSUE_DATA": "Actualizează datele problemei"
},
"D_CONFIRM_DELETE": {
"MSG": "Dorești să ștergi sarcina \"{{title}}\"?",
"OK": "Delete"
"OK": "Șterge"
},
"D_CONFIRM_SHORT_SYNTAX_NEW_TAG": {
"MSG": "Do you want to create the new tag {{tagsTxt}}?",
"OK": "Create tag"
"MSG": "DDorești să creezi eticheta nouă {{tagsTxt}}?",
"OK": "Creează etichetă"
},
"D_CONFIRM_SHORT_SYNTAX_NEW_TAGS": {
"MSG": "Do you want to create the new tags {{tagsTxt}}?",
"OK": "Create tags"
"MSG": "Dorești să creezi etichetele noi {{tagsTxt}}?",
"OK": "Creează etichete"
},
"D_DEADLINE": {
"ADD_TIME": "Adaugă ora",
@ -1549,28 +1573,28 @@
"SET_DEADLINE": "Setează termen limită"
},
"D_REMINDER_VIEW": {
"ADD_ALL_TO_TODAY": "Add all to Today",
"ADD_TO_TODAY": "Add to Today",
"ADD_ALL_TO_TODAY": "Adaugă-le pe toate la Astăzi",
"ADD_TO_TODAY": "Adaugă la Astăzi",
"CLEAR_ALL_REMINDERS": "Șterge toate memento-urile",
"CLEAR_REMINDER": "Șterge memento",
"COMPLETE": "Complete",
"COMPLETE_ALL": "Complete all",
"COMPLETE": "Finalizează",
"COMPLETE_ALL": "Finalizează-le pe toate",
"DEADLINE_REMINDER": "Memento termen limită",
"DEADLINE_SECTION": "Termene limită",
"DISMISS_ALL_REMINDERS_KEEP_TODAY": "Dismiss All Reminders (Keep in Today)",
"DISMISS_REMINDER_KEEP_TODAY": "Dismiss Reminder (Keep in Today)",
"DONE": "Done",
"DISMISS_ALL_REMINDERS_KEEP_TODAY": "Înlătură toate mementourile (păstrează în Astăzi)",
"DISMISS_REMINDER_KEEP_TODAY": "Înlătură memento (păstrează în Astăzi)",
"DONE": "Realizat",
"DUE_SECTION": "Planificate",
"DUE_TASK": "Reminder for planned task",
"DUE_TASKS": "Reminder for planned tasks",
"RESCHEDULE_EDIT": "Edit (reschedule)",
"RESCHEDULE_UNTIL_TOMORROW": "Reschedule for tomorrow",
"SNOOZE": "Snooze",
"SNOOZE_ALL": "Snooze all",
"START": "Start",
"TASK_REMINDERS": "Memento-uri sarcini",
"UNSCHEDULE": "Unschedule",
"UNSCHEDULE_ALL": "Unschedule all"
"DUE_TASK": "Memento sarcină planificată",
"DUE_TASKS": "Memento sarcini planificate",
"RESCHEDULE_EDIT": "Editează (planifică din nou)",
"RESCHEDULE_UNTIL_TOMORROW": "Planifică din nou pentru mîine",
"SNOOZE": "Amînă",
"SNOOZE_ALL": "Amînă-le pe toate",
"START": "Începe",
"TASK_REMINDERS": "Mementouri sarcini",
"UNSCHEDULE": "Șterge planificarea",
"UNSCHEDULE_ALL": "Șterge planificarea tuturor"
},
"D_SCHEDULE_TASK": {
"QA_NEXT_MONTH": "Schedule next month",
@ -1589,9 +1613,9 @@
"UNSCHEDULE": "Unschedule"
},
"D_SELECT_DATE_AND_TIME": {
"DATE": "Date",
"TIME": "Time",
"TITLE": "Select Date and Time"
"DATE": "Dată",
"TIME": "Oră",
"TITLE": "Alege data și ora"
},
"D_TIME": {
"ADD_FOR_OTHER_DAY": "Add time spent for other day",
@ -1671,18 +1695,18 @@
"OK": "Remove completely"
},
"D_CONFIRM_UPDATE_INSTANCES": {
"CANCEL": "Only future tasks",
"MSG": "There are {{tasksNr}} instances created for this recurring task. Do you want to update all of them with the new defaults or just future tasks?",
"OK": "Update all instances"
"CANCEL": "Doar sarcinile viitoare",
"MSG": "Există {{tasksNr}} instanțe create pentru această sarcină recurentă. Dorești să le actualizezi pe toate cu noile valori implicite sau doar să actualizezi sarcinile viitoare?",
"OK": "Actualizează toate instanțele"
},
"D_SKIP_INSTANCE": {
"MSG": "Skip the recurring task on {{date}}? This will prevent the task from being created on this date only.",
"MSG": "Omiți sarcina recurentă pe {{date}}? Acest lucru va preveni crearea sarcinii doar pe această dată.",
"OK": "Omite"
},
"D_EDIT": {
"ADD": "Add Recurring Task Config",
"ADVANCED_CFG": "Advanced configuration",
"EDIT": "Edit Recurring Task Config",
"ADD": "Adaugă configurație sarcină recurentă",
"ADVANCED_CFG": "Configurare avansată",
"EDIT": "Editează configurația sarcinii recurente",
"HEATMAP_LABEL": "Activitate",
"TAG_LABEL": "Etichete",
"TIME_SPENT_THIS_MONTH": "Luna aceasta",
@ -1690,105 +1714,105 @@
"TIME_SPENT_TOTAL": "Total"
},
"F": {
"C_DAY": "Day",
"C_MONTH": "Month",
"C_WEEK": "Week",
"C_YEAR": "Year",
"DEFAULT_ESTIMATE": "Default Estimate",
"DISABLE_AUTO_UPDATE_SUBTASKS": "Disable auto-updating subtasks",
"DISABLE_AUTO_UPDATE_SUBTASKS_DESCRIPTION": "Do not update inherited subtasks automatically when the newest instance changes",
"FRIDAY": "Friday",
"INHERIT_SUBTASKS": "Inherit subtasks",
"INHERIT_SUBTASKS_DESCRIPTION": "When enabled, subtasks from the most recent task instance will be recreated with the recurring task",
"MONDAY": "Monday",
"NOTES": "Default notes",
"Q_CUSTOM": "Custom recurring config",
"Q_DAILY": "Every day",
"Q_MONDAY_TO_FRIDAY": "Every Monday through Friday",
"Q_MONTHLY_CURRENT_DATE": "Every month on the day {{dateDayStr}}",
"C_DAY": "Zi",
"C_MONTH": "Lună",
"C_WEEK": "Săptămînă",
"C_YEAR": "An",
"DEFAULT_ESTIMATE": "Estimare implicită",
"DISABLE_AUTO_UPDATE_SUBTASKS": "Dezactivează auto-actualizarea subsarcinilor",
"DISABLE_AUTO_UPDATE_SUBTASKS_DESCRIPTION": "Nu actualiza automat subsarcinile moștenate cînd instanța cea mai nouă se modifică",
"FRIDAY": "vineri",
"INHERIT_SUBTASKS": "Moștenește subsarcini",
"INHERIT_SUBTASKS_DESCRIPTION": "Cînd este activat, subsarcinile din instanța cea mai recentă a sarcinii vor fi recreate cu sarcina recurentă",
"MONDAY": "luni",
"NOTES": "Notițe implicite",
"Q_CUSTOM": "Configurare recurentă personalizată",
"Q_DAILY": "În fiecare zi",
"Q_MONDAY_TO_FRIDAY": "În fiecare zi de luni pînă vineri",
"Q_MONTHLY_CURRENT_DATE": "În fiecare lună în ziua {{dateDayStr}}",
"Q_MONTHLY_FIRST_DAY": "În fiecare lună în prima zi",
"Q_MONTHLY_LAST_DAY": "În fiecare lună în ultima zi",
"Q_WEEKLY_CURRENT_WEEKDAY": "Every week on {{weekdayStr}}",
"Q_YEARLY_CURRENT_DATE": "Every year on the {{dayAndMonthStr}}",
"QUICK_SETTING": "Recurring Config",
"REMIND_AT": "Remind at",
"REMIND_AT_PLACEHOLDER": "Select when to remind",
"SKIP_FOR_DATE": "Skip for {{date}}",
"SKIP_INSTANCE": "Skip for today",
"REPEAT_CYCLE": "Recur cycle",
"REPEAT_EVERY": "Recur every",
"SATURDAY": "Saturday",
"SCHEDULE_TYPE_LABEL": "Schedule type",
"Q_WEEKLY_CURRENT_WEEKDAY": "În fiecare săptămînă {{weekdayStr}}",
"Q_YEARLY_CURRENT_DATE": "În fiecare an pe {{dayAndMonthStr}}",
"QUICK_SETTING": "Configurație recurentă",
"REMIND_AT": "Amintește la",
"REMIND_AT_PLACEHOLDER": "Selectează cînd să fie amintit",
"SKIP_FOR_DATE": "Omite pentru {{date}}",
"SKIP_INSTANCE": "Omite pentru astăzi",
"REPEAT_CYCLE": "Ciclu de repetare",
"REPEAT_EVERY": "Repetă la fiecare",
"SATURDAY": "sîmbătă",
"SCHEDULE_TYPE_LABEL": "Tip programare",
"SKIP_OVERDUE": "Omite instanțele întîrziate",
"SKIP_OVERDUE_DESCRIPTION": "Cînd este activat, dacă aplicația este deschisă după ce a fost omisă o apariție programată (de ex. o sarcină săptămînală în care azi nu este o zi programată), instanța ratată va fi omisă silențios în loc să fie creată ca sarcină întîrziată",
"START_DATE": "Start date",
"START_TIME": "Scheduled start time",
"START_TIME_DESCRIPTION": "E.g. 15:00. Leave blank for an all day task",
"SUNDAY": "Sunday",
"THURSDAY": "Thursday",
"TITLE": "Task title",
"TUESDAY": "Tuesday",
"WEDNESDAY": "Wednesday"
"START_DATE": "Dată de început",
"START_TIME": "Timpul programat de început",
"START_TIME_DESCRIPTION": "De ex. 15:00. Lasă necompletat pentru o sarcină pe toată ziua",
"SUNDAY": "duminică",
"THURSDAY": "joi",
"TITLE": "Nume sarcină",
"TUESDAY": "marți",
"WEDNESDAY": "miercuri"
},
"SNACK_REPEAT_DIALOG_FAIL": "Nu s-a putut deschide dialogul de configurare repetare. Puteți configura din meniul sarcinii."
},
"TASK_VIEW": {
"CUSTOMIZER": {
"DEADLINE_NEXT_MONTH": "Due Next Month",
"DEADLINE_NEXT_WEEK": "Due Next Week",
"DEADLINE_THIS_MONTH": "Due This Month",
"DEADLINE_THIS_WEEK": "Due This Week",
"DEADLINE_TODAY": "Due Today",
"DEADLINE_TOMORROW": "Due Tomorrow",
"ENTER_PROJECT": "Filter Projects",
"ENTER_TAG": "Filter Tag",
"ESTIMATED_TIME": "Estimated Time",
"FILTER_BY": "Filter By",
"FILTER_DEADLINE": "Deadline",
"FILTER_DEFAULT": "No Filter",
"FILTER_ESTIMATED_TIME": "Estimated Time",
"FILTER_NOT_SPECIFIED": "Not specified",
"FILTER_PROJECT": "Project",
"FILTER_SCHEDULED_DATE": "Scheduled Date",
"FILTER_TAG": "Tag",
"FILTER_TIME_SPENT": "Time Spent",
"GROUP_BY": "Group By",
"GROUP_DEADLINE": "Deadline",
"GROUP_DEADLINE_NONE": "No deadline",
"GROUP_DEFAULT": "No Group",
"GROUP_PROJECT": "Project",
"GROUP_SCHEDULED_DATE": "Scheduled Date",
"GROUP_TAG": "Tag",
"RESET_ALL": "Reset All",
"DEADLINE_NEXT_MONTH": "Scadente luna viitoare",
"DEADLINE_NEXT_WEEK": "Scadente săptămîna viitoare",
"DEADLINE_THIS_MONTH": "Scadente luna aceasta",
"DEADLINE_THIS_WEEK": "Scadente săptămîna aceasta",
"DEADLINE_TODAY": "Scadente astăzi",
"DEADLINE_TOMORROW": "Scadente mîine",
"ENTER_PROJECT": "Filtrează proiecte",
"ENTER_TAG": "Filtrează etichete",
"ESTIMATED_TIME": "Timp estimat",
"FILTER_BY": "Filtrează după",
"FILTER_DEADLINE": "Scadență",
"FILTER_DEFAULT": "Fără filtru",
"FILTER_ESTIMATED_TIME": "Timp estimat",
"FILTER_NOT_SPECIFIED": "Nu este specificat",
"FILTER_PROJECT": "Proiect",
"FILTER_SCHEDULED_DATE": "Dată programată",
"FILTER_TAG": "Etichetă",
"FILTER_TIME_SPENT": "Timp petrecut",
"GROUP_BY": "Grupare după",
"GROUP_DEADLINE": "Scadență",
"GROUP_DEADLINE_NONE": "Fără scadență",
"GROUP_DEFAULT": "Fără grup",
"GROUP_PROJECT": "Proiect",
"GROUP_SCHEDULED_DATE": "Dată programată",
"GROUP_TAG": "Etichetă",
"RESET_ALL": "Resetează tot",
"SAVE_SORT": "Salvează ca implicit",
"SCHEDULED_NEXT_MONTH": "Next Month",
"SCHEDULED_NEXT_WEEK": "Next Week",
"SCHEDULED_THIS_MONTH": "This Month",
"SCHEDULED_THIS_WEEK": "This Week",
"SCHEDULED_NEXT_MONTH": "Luna viitoare",
"SCHEDULED_NEXT_WEEK": "Săptămîna viitoare",
"SCHEDULED_THIS_MONTH": "Luna aceasta",
"SCHEDULED_THIS_WEEK": "Săptămîna aceasta",
"SCHEDULED_TODAY": "Astăzi",
"SCHEDULED_TOMORROW": "Mîine",
"SORT": "Sortare",
"SORT_CREATION_DATE": "Creation Date",
"SORT_DEADLINE": "Deadline",
"SORT_DEFAULT": "Default",
"SORT_NAME": "Name",
"SORT_SCHEDULED_DATE": "Scheduled Date",
"TIME_1HOUR": "> 1 Hour",
"TIME_2HOUR": "> 2 Hours",
"TIME_10MIN": "> 10 Minutes",
"TIME_30MIN": "> 30 Minutes",
"TIME_SPENT": "Time Spent"
"SORT_CREATION_DATE": "Dată de creare",
"SORT_DEADLINE": "Scadență",
"SORT_DEFAULT": "Implicit",
"SORT_NAME": "Nume",
"SORT_SCHEDULED_DATE": "Dată programată",
"TIME_1HOUR": "> 1 oră",
"TIME_2HOUR": "> 2 ore",
"TIME_10MIN": "> 10 minute",
"TIME_30MIN": "> 30 minute",
"TIME_SPENT": "Timp petrecut"
}
},
"TIME_TRACKING": {
"B": {
"ALREADY_DID": "I already did",
"SNOOZE": "Snooze {{time}}"
"ALREADY_DID": "Deja am făcut",
"SNOOZE": "Amînă {{time}}"
},
"B_TTR": {
"ADD_TO_TASK": "Add to Task",
"MSG": "You have not been tracking time for {{time}}",
"MSG_WITHOUT_TIME": "You have not been tracking time"
"ADD_TO_TASK": "Adaugă la sarcină",
"MSG": "Nu ai ținut evidența timpului pentru {{time}}",
"MSG_WITHOUT_TIME": "Nu ai ținut evidența timpului"
},
"D_ARCHIVE_COMPRESS": {
"BTN_COMPRESS": "Comprimă arhiva",
@ -1928,10 +1952,10 @@
"DO_IT": "Fă-l!",
"DONT_SHOW_AGAIN": "Nu mai afișa din nou",
"DUPLICATE": "Duplică",
"DURATION_DESCRIPTION": "ex: \"5h 23m\" care rezultă în 5 ore și 23 minute",
"DURATION_DESCRIPTION": "d.e. \"5h 23m\" care rezultă în 5 ore și 23 minute",
"EDIT": "Editează",
"ENABLED": "Activat",
"EXAMPLE_VAL": "ex: 32m",
"EXAMPLE_VAL": "d.e. 32m",
"EXTENSION_INFO": "Te rog <a target=\"_blank\" href=\"https://chrome.google.com/webstore/detail/super-productivity/ljkbjodfmekklcoibdnhahlaalhihmlb\"> descarcă extensia Chrome</a> pentru a permite comunicarea cu API-ul Jira și tratarea timpului mort. Acest lucru nu funcționează pe mobil. <strong>Fără extensie, aceste funcții nu vor funcționa!</strong>",
"HIDE": "Ascunde",
"ICON_INP_DESCRIPTION": "Toate emoji-urile UTF-8 sînt de asemenea suportate!",
@ -1942,7 +1966,7 @@
"NEXT": "Următorul",
"NO_CON": "Momentan ești offline. Te rog reconectează-te la internet.",
"NONE": "Niciunul",
"OK": "Ok",
"OK": "OK",
"OPEN_IN_BROWSER": "Deschide în navigatorul web",
"OVERDUE": "Restante",
"PASSWORD_STRENGTH_FAIR": "Acceptabilă",
@ -1962,25 +1986,27 @@
},
"GCF": {
"APP_FEATURES": {
"BOARDS": "Boards",
"DONATE_PAGE": "Donate page",
"BOARDS": "Table",
"DONATE_PAGE": "Pagină de donații",
"FINISH_DAY": "Încheie ziua",
"FOCUS_MODE": "Focus mode",
"FOCUS_MODE": "Mod concentrare",
"HABITS": "Obiceiuri",
"HELP": "Enable or disable specific app features across the UI.",
"ISSUES_PANEL": "Issues panel",
"PLANNER": "Planner",
"PROJECT_NOTES": "Project notes",
"HELP": "Activează sau dezactivează anumite funcții ale aplicației din interfață.",
"ISSUES_PANEL": "Panou probleme",
"PLANNER": "Planificator",
"PROJECT_NOTES": "Notițe proiect",
"SCHEDULE": "Orar",
"SCHEDULE_DAY_PANEL": "Schedule day panel",
"SCHEDULE_DAY_PANEL": "Panou programare zi",
"SEARCH": "Caută",
"SYNC_BUTTON": "Sync button",
"TIME_TRACKING": "Stopwatch time tracking",
"TITLE": "App Features",
"USER_PROFILES": "User profiles (experimental)",
"USER_PROFILES_HINT": "Allows you to create and switch between different user profiles, each with separate settings, tasks, and sync configurations. The profile management button will appear in the top-right corner when enabled. Note: Disabling this feature will hide the UI but preserve your profile data. (Experimental feature. No guarantees. Make sure to have a backup!)",
"USER_PROFILES_WARNING": "<strong>Funcție experimentală:</strong> Această funcție este încă în dezvoltare și poate fi eliminată sau modificată semnificativ într-o actualizare viitoare. Folosește pe propriul risc și asigură-te că ai backup-uri ale datelor."
},
"SYNC_BUTTON": "Buton sincronizare",
"TIME_TRACKING": "Cronometru urmărire timp",
"TITLE": "Funcții aplicație",
"USER_PROFILES": "Profiluri utilizator (experimental — ar putea fi eliminate)",
"USER_PROFILES_HINT": "Îți permite să creezi și să comuti între diferite profiluri de utilizator, fiecare cu setări, sarcini și configurații de sincronizare separate. Butonul de gestionare a profilurilor va apărea în colțul din dreapta sus cînd este activat. Notă: Dezactivarea acestei funcții va ascunde interfața dar va păstra datele profilului. (Funcție experimentală. Fără garanții. Asigură-te că ai o copie de rezervă!)",
"USER_PROFILES_WARNING": "<strong>Funcție experimentală:</strong> Această funcție este încă în dezvoltare și ar putea fi eliminată sau modificată semnificativ într-o actualizare viitoare. Folosește cu grijă și asigură-te că ai copii de rezervă ale datelor tale.",
"EXPERIMENTAL_WARNING_TITLE": "Feature Experimental",
"EXPERIMENTAL_WARNING_MSG": "Această funcție este experimentală. Poate conține erori, poate fi modificată semnificativ sau poate fi eliminată într-o actualizare viitoare. Asigură-te că ai copii de rezervă ale datelor tale.<br/><br/>Vrei să o activezi?"
},
"AUTO_BACKUPS": {
"HELP": "Automatically save all data to your app folder to have it ready if something goes wrong.",
"LABEL_IS_ENABLED": "Enable automatic backups",
@ -1995,8 +2021,8 @@
"SHOW_BANNER_THRESHOLD": "Show a notification X before the event (blank for disabled)"
},
"CLIPBOARD_IMAGES": {
"DELETE": "Delete image",
"DELETE_ALL": "Delete All",
"DELETE": "Șterge imaginea",
"DELETE_ALL": "Șterge-le pe toate",
"DELETE_ALL_SUCCESS": "Toate imaginile din clipboard șterse cu succes",
"DELETE_SUCCESS": "Imagine din clipboard ștearsă cu succes",
"ERROR_DELETING": "Ștergerea imaginii din clipboard a eșuat",
@ -2013,14 +2039,14 @@
"PATH_LABEL": "Cale stocare imagini",
"PATH_PLACEHOLDER": "Lasă gol pentru locația implicită",
"PATH_TITLE": "Locație stocare",
"SELECT_PATH": "Select Folder",
"SELECT_PATH": "Selectează dosarul",
"SELECT_PATH_TITLE": "Selectează dosarul pentru imagini clipboard",
"SORT_BY": "Sort by",
"SORT_LARGEST": "Largest first",
"SORT_NEWEST": "Newest first",
"SORT_OLDEST": "Oldest first",
"SORT_SMALLEST": "Smallest first",
"TITLE": "Imagini clipboard",
"SORT_BY": "Sortează după",
"SORT_LARGEST": "Cele mai mari primele",
"SORT_NEWEST": "Cele mai noi primele",
"SORT_OLDEST": "Cele mai vechi primele",
"SORT_SMALLEST": "Cele mai mici primele",
"TITLE": "Imagini Clipboard",
"TOTAL_IMAGES": "Total imagini",
"TOTAL_SIZE": "Dimensiune totală"
},
@ -2149,6 +2175,7 @@
"TIME_LOCALE_IT_IT": "Italienesc: 24 de ore, DD/MM/YYYY",
"TIME_LOCALE_JA_JP": "Japonez: 24 de ore, YYYY/MM/DD",
"TIME_LOCALE_KO_KR": "Korean: 12 ore AM/PM, YYYY. MM. DD",
"TIME_LOCALE_PL_PL": "Polonez: 24 de ore, DD.MM.YYYY",
"TIME_LOCALE_PT_BR": "Portughez (Brazilia): 24 de ore, DD/MM/YYYY",
"TIME_LOCALE_RO_MD": "Românesc (Moldova): 24 de ore, DD.MM.YYYY",
"TIME_LOCALE_RO_RO": "Românesc: 24 de ore, DD.MM.YYYY",
@ -2218,10 +2245,10 @@
"L_LUNCH_BREAK_START": "Lunch break start",
"L_WORK_END": "Work Day End",
"L_WORK_START": "Work Day Start",
"LUNCH_BREAK_START_END_DESCRIPTION": "ex: 13:00",
"LUNCH_BREAK_START_END_DESCRIPTION": "d.e. 13:00",
"MONTH": "Lună",
"TITLE": "Programare",
"WORK_START_END_DESCRIPTION": "ex: 17:00"
"WORK_START_END_DESCRIPTION": "d.e. 17:00"
},
"SHORT_SYNTAX": {
"HELP": "<p>Here you can control short syntax options when creating a task</p>",
@ -2235,9 +2262,9 @@
"URL_BEHAVIOR_LABEL": "URL behavior when adding tasks"
},
"SOUND": {
"BREAK_REMINDER_SOUND": "Take a break reminder sound",
"DONE_SOUND": "Task completed sound",
"IS_INCREASE_DONE_PITCH": "Increase pitch for every task completed",
"BREAK_REMINDER_SOUND": "Sunet memento de luat o pauză",
"DONE_SOUND": "Sunet sarcină finalizată",
"IS_INCREASE_DONE_PITCH": "Crește tonul pentru fiecare sarcină finalizată",
"TITLE": "Sunet",
"TRACK_TIME_SOUND": "Sunet memento de urmărire a timpului",
"VOLUME": "Volum"
@ -2646,18 +2673,19 @@
"CREATE_NEW_PROFILE": "Creează un nou profil",
"CREATED": "Creat:",
"DELETE_PROFILE": "Șterge profilul",
"DEPRECATION_WARNING": "Funcția experimentală Profile utilizator va fi eliminată într-o actualizare viitoare. Te rog să îți exporti datele de profil ca backup.",
"DIALOG_TITLE": "Administrează profilurile de utilizator",
"EXISTING_PROFILES": "Profiluri existente",
"EXPORT_PROFILE": "Exportă profilul",
"MANAGE_PROFILES": "Gestionează profilurile...",
"PROFILE_NAME": "Nume profil",
"PROFILE_NAME_PLACEHOLDER": "ex: Muncă, Personal",
"PROFILE_NAME_PLACEHOLDER": "d.e. Muncă, Personal",
"RENAME": "Redenumește",
"SAVE": "Salvează"
},
"V": {
"E_DATETIME": "Valoare întrodusă nu este o dată și oră!",
"E_DURATION": "Te rog întrodu o durată validă (ex: 1h)",
"E_DURATION": "Te rog întrodu o durată validă (d.e. 1h)",
"E_MAX": "Nu trebuie să fie mai mare decît {{val}}",
"E_MAX_LENGTH": "Trebuie să aibă cel mult {{val}} caractere",
"E_MIN": "Nu trebuie să fie mai mică decît {{val}}",

View file

@ -34,7 +34,7 @@
"BY": "după",
"NO_RESULTS": "Nicio imagine găsită. Te rog încearcă un alt termen de căutare.",
"SEARCH_LABEL": "Caută imagini",
"SEARCH_PLACEHOLDER": "ex: natură, munți, abstract",
"SEARCH_PLACEHOLDER": "d.e. natură, munți, abstract",
"TITLE": "Alege o imagine de fundal din Unsplash"
},
"DONATE_PAGE": {
@ -94,8 +94,8 @@
"ADD_NEW_PANEL": "Adaugă un nou panou",
"BACKLOG_TASK_FILTER_ALL": "Toate",
"BACKLOG_TASK_FILTER_NO_BACKLOG": "Exclude",
"BACKLOG_TASK_FILTER_ONLY_BACKLOG": "Doar restante",
"BACKLOG_TASK_FILTER_TYPE": "Sarcini restante",
"BACKLOG_TASK_FILTER_ONLY_BACKLOG": "Doar backlog",
"BACKLOG_TASK_FILTER_TYPE": "Sarcini din backlog",
"COLUMNS": "Coloane",
"TAGS_EXCLUDED": "Etichete excluse",
"TAGS_REQUIRED": "Etichete obligatorii",
@ -167,8 +167,18 @@
"EVENT_STR": "eveniment",
"EVENTS_STR": "evenimente"
},
"CONTEXT_MENU": {
"CREATE_TASK": "Creează ca sarcină",
"OPEN_IN_CALENDAR": "Deschide în calendar",
"RESCHEDULE": "Replanifică",
"DELETE_EVENT": "Șterge evenimentul"
},
"S": {
"CAL_PROVIDER_ERROR": "Eroare furnizor calendar: {{errTxt}}"
"CAL_PROVIDER_ERROR": "Eroare furnizor calendar: {{errTxt}}",
"EVENT_HIDDEN": "Eveniment calendar ascuns",
"EVENT_RESCHEDULED": "Eveniment reprogramat",
"EVENT_DELETED": "Eveniment șters",
"TIME_BLOCK_ERROR": "Nu s-a putut sincroniza blocul de timp: {{errTxt}}"
}
},
"CLIPBOARD_IMAGE": {
@ -265,7 +275,7 @@
},
"GITEA": {
"FORM": {
"HOST": "Gazdă (ex: https://try.gitea.io)",
"HOST": "Gazdă (d.e. https://try.gitea.io)",
"REPO_FULL_NAME": "Nume utilizator sau nume organizație/proiect",
"REPO_FULL_NAME_DESCRIPTION": "Poate fi găsit ca parte a URL-ului când vizualizezi proiectul într-un navigator web.",
"SCOPE": "Domeniu de aplicare",
@ -303,7 +313,7 @@
"FILTER_USER": "Filtrează după numele de utilizator",
"GITLAB_BASE_URL": "URL de bază personalizat GitLab (opțional)",
"PROJECT": "nume utilizator/proiect",
"PROJECT_HINT": "ex: super-productivity/super-productivity",
"PROJECT_HINT": "d.e. super-productivity/super-productivity",
"SCOPE": "Domeniu de aplicare",
"SCOPE_ALL": "Toate",
"SCOPE_ASSIGNED": "Atribuite mie",
@ -421,8 +431,16 @@
"DIALOG": {
"ADVANCED_CONFIG": "Configurare avansată",
"CONNECTED": "Conectat",
"DELETE_CONFIRM": "Dorești să ștergi acest furnizor de probleme? Ștergerea înseamnă că <strong>toate sarcinile de probleme importate anterior vor pierde referința</strong>. Aceasta nu poate fi anulată!",
"DISCONNECT": "Deconectează"
"DELETE_CONFIRM": "Dorești să ștergi acest furnizor de probleme? Ștergerea înseamnă că <strong>toate sarcinile de probleme importate anterior vor pierde referința</strong>. Aceasta acțiune nu poate fi anulată!",
"DISCONNECT": "Deconectează",
"EDIT_TITLE": "Editează configurația {{title}}",
"LOAD_OPTIONS": "Încarcă opțiunile",
"LOAD_OPTIONS_FAILED": "Nu s-au putut încărca opțiunile",
"LOADING_OPTIONS": "Se încarcă opțiunile...",
"OPTIONS_LOADED": "Opțiuni încărcate",
"RELOAD_OPTIONS": "Reîncarcă opțiunile",
"SETUP_TITLE": "Editează configurația {{title}}",
"TEST_CONNECTION": "Testează conexiunea"
}
},
"ISSUE_PANEL": {
@ -432,37 +450,37 @@
},
"JIRA": {
"BANNER": {
"BLOCK_ACCESS_MSG": "Jira: To prevent API lock out, access has been blocked by Super Productivity. You probably should check your Jira settings!",
"BLOCK_ACCESS_UNBLOCK": "Unblock"
"BLOCK_ACCESS_MSG": "Jira: Pentru a preveni blocarea API-ului, accesul a fost blocat de Super Productivity. Probabil ar trebui să verifici setările Jira!",
"BLOCK_ACCESS_UNBLOCK": "Deblochează"
},
"CFG_CMP": {
"ALWAYS_ASK": "Always open dialog",
"DO_NOT": "Don't transition",
"DONE": "Status for completing task",
"ENABLE": "Enable Jira integration",
"ENABLE_TRANSITIONS": "Enable Transition Handling",
"IN_PROGRESS": "Status for starting task",
"LOAD_SUGGESTIONS": "Load Suggestions",
"MAP_CUSTOM_FIELDS": "Load Story Points",
"MAP_CUSTOM_FIELDS_INFO": "Unfortunately some of Jira's data is saved under custom fields which are different for every installation. To include this data, you need to select the proper custom field for it. Currently there is only the story points field that needs to be mapped.",
"OPEN": "Status for pausing task",
"SELECT_ISSUE_FOR_TRANSITIONS": "Select issue to load available transitions",
"STORY_POINTS": "Story Points Field Name",
"TRANSITION": "Transition Handling"
"ALWAYS_ASK": "Deschide mereu dialogul",
"DO_NOT": "Nu tranziționa",
"DONE": "Stare pentru finalizarea sarcinii",
"ENABLE": "Activează integrarea Jira",
"ENABLE_TRANSITIONS": "Activează gestionarea tranzițiilor",
"IN_PROGRESS": "Stare pentru începerea sarcinii",
"LOAD_SUGGESTIONS": "Încarcă sugestiile",
"MAP_CUSTOM_FIELDS": "Încarcă Story Points",
"MAP_CUSTOM_FIELDS_INFO": "Din păcate unele din datele Jira sunt salvate sub câmpuri particulare (custom fields) care sunt diferite pentru fiecare instalare. Pentru a include aceste date, trebuie să selectezi câmpul personalizat adecvat. Momentan, există doar câmpul pentru Story Points care trebuie asociat.",
"OPEN": "Stare pentru punerea pe pauză a sarcinii",
"SELECT_ISSUE_FOR_TRANSITIONS": "Selectează problema pentru a încărca tranzițiile disponibile",
"STORY_POINTS": "Nume câmp Story Points",
"TRANSITION": "Gestionarea tranzițiilor"
},
"DIALOG_CONFIRM_ASSIGNMENT": {
"MSG": "<strong>{{summary}}</strong> is currently assigned to <strong>{{assignee}}</strong>. Do you want to assign it to yourself?",
"OK": "Do it!"
"MSG": "<strong>{{summary}}</strong> Este asociat curent cu <strong>{{assignee}}</strong>. Dorești să îl asociezi pentru tine?",
"OK": "Fă-o!"
},
"DIALOG_INITIAL": {
"TITLE": "Setup Jira for Project"
"TITLE": "Configurează Jira pentru proiect"
},
"DIALOG_TRANSITION": {
"CHOOSE_STATUS": "Choose status to assign",
"CURRENT_ASSIGNEE": "Current Assignee:",
"CURRENT_STATUS": "Current Status:",
"TASK_NAME": "Task name:",
"TITLE": "Jira: Update Status"
"CHOOSE_STATUS": "Alege starea de atribuit",
"CURRENT_ASSIGNEE": "Atribuit curent:",
"CURRENT_STATUS": "Starea curentă:",
"TASK_NAME": "Nume sarcină:",
"TITLE": "Jira: Actualizează starea"
},
"DIALOG_WORKLOG": {
"CHECKBOXES": {
@ -929,32 +947,32 @@
"TEXT": "<p>Orarul îți oferă o viziune de ansamblu despre cum se desfășoară sarcinile planificate în timp. Este generat automat din sarcinile tale și necesită doar <strong>estimări de timp</strong> pentru a funcționa.</p><p>Două lucruri sunt distinse: <strong>Sarcini programate</strong>, care sunt afișate la ora planificată și <strong>Sarcini obișnuite</strong>, care ar trebui să curgă în jurul acestor evenimente fixe.</p><p>Dacă setezi o oră de început și sfârșit a lucrului (recomandat), sarcinile obișnuite nu vor fi niciodată plasate în afara acestor limite.</p>",
"TITLE": "Orar"
},
"END": "Work End",
"INSERT_BEFORE": "Before",
"LUNCH_BREAK": "Lunch Break",
"MONTH": "Month",
"NO_TASKS": "Currently there are no tasks. Please add some tasks via the plus (+) button in the top bar.",
"NOW": "Now",
"PLAN_END_DAY": "End of {{date}}",
"PLAN_START_DAY": "Start of {{date}}",
"SHIFT_KEY_INFO": "Hold Shift to toggle day planning mode",
"START": "Work Start"
"END": "Finalul muncii",
"INSERT_BEFORE": "Înainte",
"LUNCH_BREAK": "Pauză de masă",
"MONTH": "Lună",
"NO_TASKS": "Momentan nu există sarcini. Adaugă câteva sarcini prin butonul plus (+) din bara de sus.",
"NOW": "Acum",
"PLAN_END_DAY": "Final de {{date}}",
"PLAN_START_DAY": "Început de {{date}}",
"SHIFT_KEY_INFO": "Ține apăsat Shift pentru a comuta modul de planificare zilnică",
"START": "Începerea muncii"
},
"SEARCH_BAR": {
"INFO": "Start typing to search for tasks",
"NO_RESULTS": "No tasks found matching your search",
"PLACEHOLDER": "Search for task or task notes"
"INFO": "Începe să tastezi pentru a căuta sarcini",
"NO_RESULTS": "Nu s-au găsit sarcini care să se potrivească căutării",
"PLACEHOLDER": "Caută sarcină sau notițe despre sarcină"
},
"SIMPLE_COUNTER": {
"D_CONFIRM_REMOVE": {
"MSG": "Deleting a simple counter, will also delete all past data tracked on it. Are you sure you want to proceed?",
"OK": "Do it!"
"MSG": "Ștergerea unui contor simplu va șterge și toate datele urmărite pe el. Ești sigur că vrei să continui?",
"OK": "Fă-o!"
},
"D_EDIT": {
"CURRENT_STREAK": "Current Streak",
"DAILY_GOAL": "Daily Goal",
"DAYS": "Days",
"L_COUNTER": "Count"
"CURRENT_STREAK": "Serie curentă",
"DAILY_GOAL": "Obiectiv zilnic",
"DAYS": "Zile",
"L_COUNTER": "Număr"
},
"FORM": {
"ADD_NEW": "Adaugă un contor simplu sau un obicei",
@ -967,7 +985,7 @@
"L_IS_HIDE_BUTTON": "Ascunde butonul din bara de unelte",
"L_STREAK_MODE": "Mod urmărire serie",
"L_STREAK_MODE_SPECIFIC_DAYS": "Zilele săptămânii",
"L_STREAK_MODE_WEEKLY_FREQUENCY": "Frecvență săptămânală (ex: de 3 ori pe săptămână)",
"L_STREAK_MODE_WEEKLY_FREQUENCY": "Frecvență săptămânală (d.e. de 3 ori pe săptămână)",
"L_TITLE": "Titlu",
"L_TRACK_STREAKS": "Urmărește seriile",
"L_TYPE": "Tip",
@ -1000,40 +1018,40 @@
"FORCE_UPLOAD": "Forțarea încărcării datelor poate duce la pierderea acestora. Ești sigur că vrei să continui?"
},
"D_AUTH_CODE": {
"FOLLOW_LINK": "Please open the following link and copy the auth code provided there into the input field below.",
"FOLLOW_LINK_MOBILE": "Please tap the button below to authorize. You will be automatically redirected back to the app.",
"GET_AUTH_CODE": "Get Authorization Code",
"L_AUTH_CODE": "Enter Auth Code",
"TITLE": "Login: {{provider}}",
"WAITING_FOR_AUTH": "Waiting for authentication..."
"FOLLOW_LINK": "Te rog deschide următoarea legătură și copiază codul de autorizare furnizat acolo în câmpul de intrare de mai jos.",
"FOLLOW_LINK_MOBILE": "Te rog apasă butonul de mai jos pentru a autoriza. Vei fi redirecționat automat înapoi la aplicație.",
"GET_AUTH_CODE": "Obține codul de autorizare",
"L_AUTH_CODE": "Introdu codul de autorizare",
"TITLE": "Autentificare: {{provider}}",
"WAITING_FOR_AUTH": "Se așteaptă autentificarea..."
},
"D_CONFLICT": {
"ADDITIONAL_INFO": "Additional Info",
"CHANGES": "Changes",
"CHANGES_SINCE_LAST_SYNC": "Changes since last sync",
"COMPARISON_RESULT": "Comparison Result",
"DATE": "Date",
"LAST_SYNC": "Last Sync:",
"LAST_SYNCED": "Last Synced",
"LAST_WRITE": "Last Write",
"ADDITIONAL_INFO": "Informații suplimentare",
"CHANGES": "Modificări",
"CHANGES_SINCE_LAST_SYNC": "Modificări de la ultima sincronizare",
"COMPARISON_RESULT": "Rezultatul comparării",
"DATE": "Dată",
"LAST_SYNC": "Ultima Sincronizare:",
"LAST_SYNCED": "Ultima sincronizare",
"LAST_WRITE": "Ultima scriere",
"LOCAL": "Local",
"LOCAL_REMOTE": "Local -> Remote",
"NEVER": "Never",
"OVERWRITE_WARNING": "WARNING: The {{targetName}} data contains approximately {{targetChanges}} changes while the {{sourceName}} data only has {{sourceChanges}} changes. Are you sure you want to overwrite the {{targetName}} data with the {{sourceName}} version? This action cannot be undone.",
"REMOTE": "Remote",
"RESULT": "Result",
"TEXT": "<p>Update from remote. Both local and remote data seem to be modified.</p>",
"TIME": "Time",
"TIMESTAMP": "Timestamp",
"TITLE": "Sync: Conflicting Data",
"USE_LOCAL": "Keep local",
"USE_REMOTE": "Keep remote",
"VECTOR_CLOCK": "Vector Clock",
"VECTOR_CLOCK_HEADING": "Vector Clock",
"VECTOR_COMPARISON_CONCURRENT": "Concurrent (True Conflict)",
"VECTOR_COMPARISON_EQUAL": "Equal",
"VECTOR_COMPARISON_LOCAL_GREATER": "Local > Remote",
"VECTOR_COMPARISON_LOCAL_LESS": "Local < Remote"
"LOCAL_REMOTE": "Local -> Distant",
"NEVER": "Niciodată",
"OVERWRITE_WARNING": "ATENȚIE: Datele {{targetName}} conțin aproximativ {{targetChanges}} modificări în timp ce datele {{sourceName}} au doar {{sourceChanges}} modificări. Ești sigur că vrei să înlocuiești datele {{targetName}} cu versiunea {{sourceName}}? Această acțiune nu poate fi anulată.",
"REMOTE": "Distant",
"RESULT": "Rezultat",
"TEXT": "<p>Actualizează de la distanță. Atât datele locale cât și cele de pe server par modificate.</p>",
"TIME": "Timp",
"TIMESTAMP": "Marca temporală",
"TITLE": "Sincronizare: Date conflictuale",
"USE_LOCAL": "Păstrează local",
"USE_REMOTE": "Păstrează distant",
"VECTOR_CLOCK": "Ceas vectorial",
"VECTOR_CLOCK_HEADING": "Ceas vectorial",
"VECTOR_COMPARISON_CONCURRENT": "Concurent (conflict adevărat)",
"VECTOR_COMPARISON_EQUAL": "Egal",
"VECTOR_COMPARISON_LOCAL_GREATER": "Local > Distant",
"VECTOR_COMPARISON_LOCAL_LESS": "Local < Distant"
},
"D_DATA_REPAIR_CONFIRM": {
"MSG": "Datele tale par deteriorate. Dorești să încerci repararea automată? Aceasta poate duce la pierderea minoră de date.",
@ -1045,28 +1063,28 @@
"UNKNOWN_ERROR": "Eroare necunoscută de sincronizare: {{err}}"
},
"D_DECRYPT_ERROR": {
"BTN_OVER_WRITE_REMOTE": "Use This Password & Overwrite Remote",
"CHANGE_PW_AND_DECRYPT": "Retry Decrypt",
"P1": "Your remote data is encrypted and decryption failed.",
"P2_FORGOT_PW": "If you <strong>forgot your password</strong>, enter the correct password and click \"Retry Decrypt\".",
"P3_PW_CHANGED": "If you <strong>changed your password on another device</strong>, enter your new password and click \"Overwrite Remote\". This will upload your local data to the server.",
"PASSWORD": "Password",
"TITLE": "Decryption Failed"
"BTN_OVER_WRITE_REMOTE": "Folosește această parolă și suprascrie-o pe cea de la distanță",
"CHANGE_PW_AND_DECRYPT": "Reîncearcă decriptarea",
"P1": "Datele tale de la distanță sunt criptate și decriptarea a eșuat.",
"P2_FORGOT_PW": "Dacă ai <strong>uitat parola</strong>, introdu parola corectă și apasă \"Reîncearcă decriptarea\".",
"P3_PW_CHANGED": "Dacă ai <strong>schimbat parola pe un alt dispozitiv</strong>, introdu noua parolă și apasă \"Suprascrie datele de la distanță\". Aceasta va încărca datele locale criptate cu această parolă pe server.",
"PASSWORD": "Parolă",
"TITLE": "Decriptarea a eșuat"
},
"D_ENTER_PASSWORD": {
"BTN_FORCE_OVERWRITE": "Use Local Data",
"BTN_SAVE_AND_SYNC": "Save & Sync",
"FORCE_OVERWRITE_CONFIRM": "This will <strong>delete all data on the server</strong> and upload your local data encrypted with this password. Continue?",
"FORCE_OVERWRITE_INFO": "If you prefer to keep your local data, you can overwrite the server instead.",
"FORCE_OVERWRITE_TITLE": "Overwrite Server Data?",
"MESSAGE": "The sync server contains encrypted data, but no encryption password is configured on this device. Please enter your encryption password to decrypt and sync your data.",
"PASSWORD_LABEL": "Encryption Password",
"TITLE": "Encryption Password Required"
"BTN_FORCE_OVERWRITE": "Folosește datele locale",
"BTN_SAVE_AND_SYNC": "Salvează și sincronizează",
"FORCE_OVERWRITE_CONFIRM": "Acest lucru va <strong>șterge toate datele de pe server</strong> și va încărca datele locale criptate cu această parolă. Continui?",
"FORCE_OVERWRITE_INFO": "Dacă preferi să păstrezi datele locale, poți suprascrie serverul în schimb.",
"FORCE_OVERWRITE_TITLE": "Suprascrii datele de pe server?",
"MESSAGE": "Serverul de sincronizare conține date criptate, dar nu este configurată nicio parolă de criptare pe acest dispozitiv. Te rog să introduci parola de criptare pentru a decripta și sincroniza datele.",
"PASSWORD_LABEL": "Parolă de criptare",
"TITLE": "Parolă de criptare necesară"
},
"D_FRESH_CLIENT_CONFIRM": {
"MESSAGE": "This appears to be a fresh installation. Remote data with {{count}} changes was found. Do you want to download and overwrite your local data with it?",
"OK": "Download Remote Data",
"TITLE": "Initial Sync"
"MESSAGE": "Aceasta pare să fie o instalare curată. Datele de la distanță cu {{count}} modificări au fost găsite. Dorești să descarci și să înlocuiești datele locale cu ele?",
"OK": "Descarcă datele de la distanță",
"TITLE": "Sincronizare inițială"
},
"D_INCOMPLETE_SYNC": {
"BTN_CLOSE_APP": "Închide aplicația",
@ -1271,13 +1289,17 @@
"SETUP_ENCRYPTION_PASSWORD_WARNING": "Folosește aceeași parolă pe toate dispozitivele. O parolă uitată poate fi resetată prin suprascrierea datelor de la distanță.",
"SETUP_ENCRYPTION_TITLE": "SuperSync: Setare parolă"
},
"NEXTCLOUD": {
"L_SERVER_URL": "Nextcloud Server URL",
"SERVER_URL_DESCRIPTION": "e.g. https://cloud.example.com or https://example.com/nextcloud",
"L_APP_PASSWORD": "App Password",
"APP_PASSWORD_DESCRIPTION": "Go to Nextcloud → Settings → Security → App passwords to generate one"
},
"TITLE": "Sync",
"WEB_DAV": {
"CONDITIONAL_HEADER_WARNING_MSG": "Your WebDAV server does not support conditional headers (If-Unmodified-Since). This means Super Productivity cannot detect concurrent changes from other devices during upload.<br><br>Sync will still work, but there is a small risk of data conflicts if you make changes on multiple devices simultaneously.",
"CONDITIONAL_HEADER_WARNING_TITLE": "Reduced Sync Safety",
"CORS_INFO": "<strong>Making it work in a web browser:</strong> Allow Super Productivity to make CORS requests to your WebDAV server. This can have negative security implications! For Nextcloud, please refer to <a href='https://github.com/nextcloud/server/issues/3131'>this GitHub thread</a> for more information. One approach to make this work on mobile is allowing \"https://app.super-productivity.com\" via the Nextcloud app <a href='https://apps.nextcloud.com/apps/webapppassword'>webapppassword<a>. Use at your own risk!</p>",
"CORS_INFO": "<strong>Making it work in a web browser:</strong> You need to allow Super Productivity to make CORS requests to your server. For Nextcloud, one approach is allowing \"https://app.super-productivity.com\" via the <a href='https://apps.nextcloud.com/apps/webapppassword'>webapppassword</a> app. Refer to <a href='https://github.com/nextcloud/server/issues/3131'>this GitHub thread</a> for more information.",
"D_SYNC_FOLDER_PATH": "Path relative to the WebDAV server root where sync files will be stored (e.g. '/super-productivity' or '/'). This is NOT your server's internal directory path.",
"INFO": "<strong>Warning:</strong> WebDAV implementations differ wildly. Super Productivity is known to work well with Nextcloud, <strong>but it may not work with your provider</strong>. Test thoroughly before relying on it.",
"INFO": "<strong>Warning:</strong> Generic WebDAV sync is provided <strong>as-is without support</strong>. If you are using Nextcloud, please use the Nextcloud sync option instead.",
"L_BASE_URL": "Base URL",
"L_PASSWORD": "Password",
"L_SYNC_FOLDER_PATH": "Sync Folder Path",
@ -1315,6 +1337,8 @@
"ERROR_PERMISSION": "File access denied. Please check your filesystem permissions.",
"ERROR_PERMISSION_FLATPAK": "File access denied. Grant filesystem permission via Flatseal or use a path inside ~/.var/app/",
"ERROR_PERMISSION_SNAP": "File access denied. Run 'snap connect super-productivity:home' or use a path inside ~/snap/super-productivity/common/",
"ERROR_REMOTE_FILE_EMPTY": "Remote sync file is empty. This can happen after an interrupted sync. Overwrite remote with your local data?",
"ERROR_REMOTE_FILE_LOCKED": "Remote sync file is locked by the server. This may resolve on the next sync attempt.",
"ERROR_UNABLE_TO_READ_REMOTE_DATA": "Error while syncing. Unable to read remote data. Maybe you enabled encryption and your local password does not match the one used to encrypt the remote data?",
"FINISH_DAY_SYNC_ERROR": "Sync error while finishing day. Please try again.",
"FRESH_CLIENT_SYNC_CANCELLED": "Sincronizare inițială anulată. Datele de la distanță nu au fost aplicate.",
@ -1405,11 +1429,11 @@
"TITLE": "Basic Settings"
},
"S": {
"ARCHIVE_CLEANUP_FAILED": "Curățarea datelor arhivate pentru eticheta ștearsă a eșuat",
"UPDATED": "Tag settings were updated"
"ARCHIVE_CLEANUP_FAILED": "A eșuat curățarea datelor arhivate pentru eticheta ștearsă",
"UPDATED": "Setările etichetei au fost actualizate"
},
"TTL": {
"ADD_NEW_TAG": "Add new Tag"
"ADD_NEW_TAG": "Adaugă etichetă nouă"
}
},
"TAG_FOLDER": {
@ -1489,47 +1513,47 @@
"ADD_SUB_TASK": "Adaugă subsarcină",
"ADD_TO_MY_DAY": "Adaugă la lista de azi",
"ADD_TO_PROJECT": "Adaugă la proiect",
"CONVERT_TO_PARENT_TASK": "Convert to parent task",
"DELETE": "Delete task",
"DELETE_REPEAT_INSTANCE": "Delete recurring task instance",
"DROP_ATTACHMENT": "Drop here to attach to \"{{title}}\"",
"DUPLICATE": "Duplicate",
"CONVERT_TO_PARENT_TASK": "Convertește în sarcină părinte",
"DELETE": "Șterge sarcina",
"DELETE_REPEAT_INSTANCE": "Șterge instanța de sarcină recurentă",
"DROP_ATTACHMENT": "Plasează aici pentru a atașa la \"{{title}}\"",
"DUPLICATE": "Duplică",
"EDIT_DEADLINE": "Editează termen limită",
"EDIT_SCHEDULED": "Reschedule",
"FOCUS_SESSION": "Start focus session",
"MARK_DONE": "Mark as completed",
"MOVE_TO_BACKLOG": "Move to backlog",
"MOVE_TO_OTHER_PROJECT": "Move to project",
"MOVE_TO_REGULAR": "Move to regular list",
"MOVE_TO_TOP": "Move to top",
"OPEN_ATTACH": "Attach file or link",
"OPEN_ISSUE": "Open in browser",
"OPEN_TIME": "Time tracking",
"EDIT_SCHEDULED": "Planifică din nou",
"FOCUS_SESSION": "Începe sesiune de concentrare",
"MARK_DONE": "Marchează ca finalizată",
"MOVE_TO_BACKLOG": "Mută în backlog",
"MOVE_TO_OTHER_PROJECT": "Mută în proiect",
"MOVE_TO_REGULAR": "Mută în lista obișnuită",
"MOVE_TO_TOP": "Mută în partea de sus",
"OPEN_ATTACH": "Atașează fișier sau legătură",
"OPEN_ISSUE": "Deschide în navigatorul web",
"OPEN_TIME": "Contorizare timp",
"REMOVE_DEADLINE": "Șterge termen limită",
"REMOVE_FROM_MY_DAY": "Elimină din lista de azi",
"SCHEDULE": "Schedule task",
"SCHEDULE": "Planifică sarcina",
"SET_DEADLINE": "Setează termen limită",
"TOGGLE_ATTACHMENTS": "Show/hide attachments",
"TOGGLE_DETAIL_PANEL": "Show/hide additional info",
"TOGGLE_DONE": "Toggle completion status",
"TOGGLE_SUB_TASK_VISIBILITY": "Toggle subtask visibility",
"TOGGLE_TAGS": "Toggle tags",
"TRACK_TIME": "Start tracking time",
"TRACK_TIME_STOP": "Pause tracking time",
"UNSCHEDULE_TASK": "Unschedule task",
"UPDATE_ISSUE_DATA": "Update issue data"
"TOGGLE_ATTACHMENTS": "Arată/Ascunde atașamente",
"TOGGLE_DETAIL_PANEL": "Arată/Ascunde informații suplimentare",
"TOGGLE_DONE": "Comută starea de finalizare",
"TOGGLE_SUB_TASK_VISIBILITY": "Comută vizibilitatea subsarcinilor",
"TOGGLE_TAGS": "Comută etichetele",
"TRACK_TIME": "Începe contorizarea timpului",
"TRACK_TIME_STOP": "Pune pe pauză contorizarea timpului",
"UNSCHEDULE_TASK": "Șterge planificarea sarcinii",
"UPDATE_ISSUE_DATA": "Actualizează datele problemei"
},
"D_CONFIRM_DELETE": {
"MSG": "Dorești să ștergi sarcina \"{{title}}\"?",
"OK": "Delete"
"OK": "Șterge"
},
"D_CONFIRM_SHORT_SYNTAX_NEW_TAG": {
"MSG": "Do you want to create the new tag {{tagsTxt}}?",
"OK": "Create tag"
"MSG": "DDorești să creezi eticheta nouă {{tagsTxt}}?",
"OK": "Creează etichetă"
},
"D_CONFIRM_SHORT_SYNTAX_NEW_TAGS": {
"MSG": "Do you want to create the new tags {{tagsTxt}}?",
"OK": "Create tags"
"MSG": "Dorești să creezi etichetele noi {{tagsTxt}}?",
"OK": "Creează etichete"
},
"D_DEADLINE": {
"ADD_TIME": "Adaugă ora",
@ -1549,28 +1573,28 @@
"SET_DEADLINE": "Setează termen limită"
},
"D_REMINDER_VIEW": {
"ADD_ALL_TO_TODAY": "Add all to Today",
"ADD_TO_TODAY": "Add to Today",
"ADD_ALL_TO_TODAY": "Adaugă-le pe toate la Astăzi",
"ADD_TO_TODAY": "Adaugă la Astăzi",
"CLEAR_ALL_REMINDERS": "Șterge toate memento-urile",
"CLEAR_REMINDER": "Șterge memento",
"COMPLETE": "Complete",
"COMPLETE_ALL": "Complete all",
"COMPLETE": "Finalizează",
"COMPLETE_ALL": "Finalizează-le pe toate",
"DEADLINE_REMINDER": "Memento termen limită",
"DEADLINE_SECTION": "Termene limită",
"DISMISS_ALL_REMINDERS_KEEP_TODAY": "Dismiss All Reminders (Keep in Today)",
"DISMISS_REMINDER_KEEP_TODAY": "Dismiss Reminder (Keep in Today)",
"DONE": "Done",
"DISMISS_ALL_REMINDERS_KEEP_TODAY": "Înlătură toate mementourile (păstrează în Astăzi)",
"DISMISS_REMINDER_KEEP_TODAY": "Înlătură memento (păstrează în Astăzi)",
"DONE": "Realizat",
"DUE_SECTION": "Planificate",
"DUE_TASK": "Reminder for planned task",
"DUE_TASKS": "Reminder for planned tasks",
"RESCHEDULE_EDIT": "Edit (reschedule)",
"RESCHEDULE_UNTIL_TOMORROW": "Reschedule for tomorrow",
"SNOOZE": "Snooze",
"SNOOZE_ALL": "Snooze all",
"START": "Start",
"TASK_REMINDERS": "Memento-uri sarcini",
"UNSCHEDULE": "Unschedule",
"UNSCHEDULE_ALL": "Unschedule all"
"DUE_TASK": "Memento sarcină planificată",
"DUE_TASKS": "Memento sarcini planificate",
"RESCHEDULE_EDIT": "Editează (planifică din nou)",
"RESCHEDULE_UNTIL_TOMORROW": "Planifică din nou pentru mâine",
"SNOOZE": "Amână",
"SNOOZE_ALL": "Amână-le pe toate",
"START": "Începe",
"TASK_REMINDERS": "Mementouri sarcini",
"UNSCHEDULE": "Șterge planificarea",
"UNSCHEDULE_ALL": "Șterge planificarea tuturor"
},
"D_SCHEDULE_TASK": {
"QA_NEXT_MONTH": "Schedule next month",
@ -1589,9 +1613,9 @@
"UNSCHEDULE": "Unschedule"
},
"D_SELECT_DATE_AND_TIME": {
"DATE": "Date",
"TIME": "Time",
"TITLE": "Select Date and Time"
"DATE": "Dată",
"TIME": "Oră",
"TITLE": "Alege data și ora"
},
"D_TIME": {
"ADD_FOR_OTHER_DAY": "Add time spent for other day",
@ -1671,18 +1695,18 @@
"OK": "Remove completely"
},
"D_CONFIRM_UPDATE_INSTANCES": {
"CANCEL": "Only future tasks",
"MSG": "There are {{tasksNr}} instances created for this recurring task. Do you want to update all of them with the new defaults or just future tasks?",
"OK": "Update all instances"
"CANCEL": "Doar sarcinile viitoare",
"MSG": "Există {{tasksNr}} instanțe create pentru această sarcină recurentă. Dorești să le actualizezi pe toate cu noile valori implicite sau doar să actualizezi sarcinile viitoare?",
"OK": "Actualizează toate instanțele"
},
"D_SKIP_INSTANCE": {
"MSG": "Skip the recurring task on {{date}}? This will prevent the task from being created on this date only.",
"MSG": "Omiți sarcina recurentă pe {{date}}? Acest lucru va preveni crearea sarcinii doar pe această dată.",
"OK": "Omite"
},
"D_EDIT": {
"ADD": "Add Recurring Task Config",
"ADVANCED_CFG": "Advanced configuration",
"EDIT": "Edit Recurring Task Config",
"ADD": "Adaugă configurație sarcină recurentă",
"ADVANCED_CFG": "Configurare avansată",
"EDIT": "Editează configurația sarcinii recurente",
"HEATMAP_LABEL": "Activitate",
"TAG_LABEL": "Etichete",
"TIME_SPENT_THIS_MONTH": "Luna aceasta",
@ -1690,105 +1714,105 @@
"TIME_SPENT_TOTAL": "Total"
},
"F": {
"C_DAY": "Day",
"C_MONTH": "Month",
"C_WEEK": "Week",
"C_YEAR": "Year",
"DEFAULT_ESTIMATE": "Default Estimate",
"DISABLE_AUTO_UPDATE_SUBTASKS": "Disable auto-updating subtasks",
"DISABLE_AUTO_UPDATE_SUBTASKS_DESCRIPTION": "Do not update inherited subtasks automatically when the newest instance changes",
"FRIDAY": "Friday",
"INHERIT_SUBTASKS": "Inherit subtasks",
"INHERIT_SUBTASKS_DESCRIPTION": "When enabled, subtasks from the most recent task instance will be recreated with the recurring task",
"MONDAY": "Monday",
"NOTES": "Default notes",
"Q_CUSTOM": "Custom recurring config",
"Q_DAILY": "Every day",
"Q_MONDAY_TO_FRIDAY": "Every Monday through Friday",
"Q_MONTHLY_CURRENT_DATE": "Every month on the day {{dateDayStr}}",
"C_DAY": "Zi",
"C_MONTH": "Lună",
"C_WEEK": "Săptămână",
"C_YEAR": "An",
"DEFAULT_ESTIMATE": "Estimare implicită",
"DISABLE_AUTO_UPDATE_SUBTASKS": "Dezactivează auto-actualizarea subsarcinilor",
"DISABLE_AUTO_UPDATE_SUBTASKS_DESCRIPTION": "Nu actualiza automat subsarcinile moștenate când instanța cea mai nouă se modifică",
"FRIDAY": "vineri",
"INHERIT_SUBTASKS": "Moștenește subsarcini",
"INHERIT_SUBTASKS_DESCRIPTION": "Când este activat, subsarcinile din instanța cea mai recentă a sarcinii vor fi recreate cu sarcina recurentă",
"MONDAY": "luni",
"NOTES": "Notițe implicite",
"Q_CUSTOM": "Configurare recurentă personalizată",
"Q_DAILY": "În fiecare zi",
"Q_MONDAY_TO_FRIDAY": "În fiecare zi de luni până vineri",
"Q_MONTHLY_CURRENT_DATE": "În fiecare lună în ziua {{dateDayStr}}",
"Q_MONTHLY_FIRST_DAY": "În fiecare lună în prima zi",
"Q_MONTHLY_LAST_DAY": "În fiecare lună în ultima zi",
"Q_WEEKLY_CURRENT_WEEKDAY": "Every week on {{weekdayStr}}",
"Q_YEARLY_CURRENT_DATE": "Every year on the {{dayAndMonthStr}}",
"QUICK_SETTING": "Recurring Config",
"REMIND_AT": "Remind at",
"REMIND_AT_PLACEHOLDER": "Select when to remind",
"SKIP_FOR_DATE": "Skip for {{date}}",
"SKIP_INSTANCE": "Skip for today",
"REPEAT_CYCLE": "Recur cycle",
"REPEAT_EVERY": "Recur every",
"SATURDAY": "Saturday",
"SCHEDULE_TYPE_LABEL": "Schedule type",
"Q_WEEKLY_CURRENT_WEEKDAY": "În fiecare săptămână {{weekdayStr}}",
"Q_YEARLY_CURRENT_DATE": "În fiecare an pe {{dayAndMonthStr}}",
"QUICK_SETTING": "Configurație recurentă",
"REMIND_AT": "Amintește la",
"REMIND_AT_PLACEHOLDER": "Selectează când să fie amintit",
"SKIP_FOR_DATE": "Omite pentru {{date}}",
"SKIP_INSTANCE": "Omite pentru astăzi",
"REPEAT_CYCLE": "Ciclu de repetare",
"REPEAT_EVERY": "Repetă la fiecare",
"SATURDAY": "sâmbătă",
"SCHEDULE_TYPE_LABEL": "Tip programare",
"SKIP_OVERDUE": "Omite instanțele întârziate",
"SKIP_OVERDUE_DESCRIPTION": "Când este activat, dacă aplicația este deschisă după ce a fost omisă o apariție programată (de ex. o sarcină săptămânală în care azi nu este o zi programată), instanța ratată va fi omisă silențios în loc să fie creată ca sarcină întârziată",
"START_DATE": "Start date",
"START_TIME": "Scheduled start time",
"START_TIME_DESCRIPTION": "E.g. 15:00. Leave blank for an all day task",
"SUNDAY": "Sunday",
"THURSDAY": "Thursday",
"TITLE": "Task title",
"TUESDAY": "Tuesday",
"WEDNESDAY": "Wednesday"
"START_DATE": "Dată de început",
"START_TIME": "Timpul programat de început",
"START_TIME_DESCRIPTION": "De ex. 15:00. Lasă necompletat pentru o sarcină pe toată ziua",
"SUNDAY": "duminică",
"THURSDAY": "joi",
"TITLE": "Nume sarcină",
"TUESDAY": "marți",
"WEDNESDAY": "miercuri"
},
"SNACK_REPEAT_DIALOG_FAIL": "Nu s-a putut deschide dialogul de configurare repetare. Puteți configura din meniul sarcinii."
},
"TASK_VIEW": {
"CUSTOMIZER": {
"DEADLINE_NEXT_MONTH": "Due Next Month",
"DEADLINE_NEXT_WEEK": "Due Next Week",
"DEADLINE_THIS_MONTH": "Due This Month",
"DEADLINE_THIS_WEEK": "Due This Week",
"DEADLINE_TODAY": "Due Today",
"DEADLINE_TOMORROW": "Due Tomorrow",
"ENTER_PROJECT": "Filter Projects",
"ENTER_TAG": "Filter Tag",
"ESTIMATED_TIME": "Estimated Time",
"FILTER_BY": "Filter By",
"FILTER_DEADLINE": "Deadline",
"FILTER_DEFAULT": "No Filter",
"FILTER_ESTIMATED_TIME": "Estimated Time",
"FILTER_NOT_SPECIFIED": "Not specified",
"FILTER_PROJECT": "Project",
"FILTER_SCHEDULED_DATE": "Scheduled Date",
"FILTER_TAG": "Tag",
"FILTER_TIME_SPENT": "Time Spent",
"GROUP_BY": "Group By",
"GROUP_DEADLINE": "Deadline",
"GROUP_DEADLINE_NONE": "No deadline",
"GROUP_DEFAULT": "No Group",
"GROUP_PROJECT": "Project",
"GROUP_SCHEDULED_DATE": "Scheduled Date",
"GROUP_TAG": "Tag",
"RESET_ALL": "Reset All",
"DEADLINE_NEXT_MONTH": "Scadente luna viitoare",
"DEADLINE_NEXT_WEEK": "Scadente săptămâna viitoare",
"DEADLINE_THIS_MONTH": "Scadente luna aceasta",
"DEADLINE_THIS_WEEK": "Scadente săptămâna aceasta",
"DEADLINE_TODAY": "Scadente astăzi",
"DEADLINE_TOMORROW": "Scadente mâine",
"ENTER_PROJECT": "Filtrează proiecte",
"ENTER_TAG": "Filtrează etichete",
"ESTIMATED_TIME": "Timp estimat",
"FILTER_BY": "Filtrează după",
"FILTER_DEADLINE": "Scadență",
"FILTER_DEFAULT": "Fără filtru",
"FILTER_ESTIMATED_TIME": "Timp estimat",
"FILTER_NOT_SPECIFIED": "Nu este specificat",
"FILTER_PROJECT": "Proiect",
"FILTER_SCHEDULED_DATE": "Dată programată",
"FILTER_TAG": "Etichetă",
"FILTER_TIME_SPENT": "Timp petrecut",
"GROUP_BY": "Grupare după",
"GROUP_DEADLINE": "Scadență",
"GROUP_DEADLINE_NONE": "Fără scadență",
"GROUP_DEFAULT": "Fără grup",
"GROUP_PROJECT": "Proiect",
"GROUP_SCHEDULED_DATE": "Dată programată",
"GROUP_TAG": "Etichetă",
"RESET_ALL": "Resetează tot",
"SAVE_SORT": "Salvează ca implicit",
"SCHEDULED_NEXT_MONTH": "Next Month",
"SCHEDULED_NEXT_WEEK": "Next Week",
"SCHEDULED_THIS_MONTH": "This Month",
"SCHEDULED_THIS_WEEK": "This Week",
"SCHEDULED_NEXT_MONTH": "Luna viitoare",
"SCHEDULED_NEXT_WEEK": "Săptămâna viitoare",
"SCHEDULED_THIS_MONTH": "Luna aceasta",
"SCHEDULED_THIS_WEEK": "Săptămâna aceasta",
"SCHEDULED_TODAY": "Astăzi",
"SCHEDULED_TOMORROW": "Mâine",
"SORT": "Sortare",
"SORT_CREATION_DATE": "Creation Date",
"SORT_DEADLINE": "Deadline",
"SORT_DEFAULT": "Default",
"SORT_NAME": "Name",
"SORT_SCHEDULED_DATE": "Scheduled Date",
"TIME_1HOUR": "> 1 Hour",
"TIME_2HOUR": "> 2 Hours",
"TIME_10MIN": "> 10 Minutes",
"TIME_30MIN": "> 30 Minutes",
"TIME_SPENT": "Time Spent"
"SORT_CREATION_DATE": "Dată de creare",
"SORT_DEADLINE": "Scadență",
"SORT_DEFAULT": "Implicit",
"SORT_NAME": "Nume",
"SORT_SCHEDULED_DATE": "Dată programată",
"TIME_1HOUR": "> 1 oră",
"TIME_2HOUR": "> 2 ore",
"TIME_10MIN": "> 10 minute",
"TIME_30MIN": "> 30 minute",
"TIME_SPENT": "Timp petrecut"
}
},
"TIME_TRACKING": {
"B": {
"ALREADY_DID": "I already did",
"SNOOZE": "Snooze {{time}}"
"ALREADY_DID": "Deja am făcut",
"SNOOZE": "Amână {{time}}"
},
"B_TTR": {
"ADD_TO_TASK": "Add to Task",
"MSG": "You have not been tracking time for {{time}}",
"MSG_WITHOUT_TIME": "You have not been tracking time"
"ADD_TO_TASK": "Adaugă la sarcină",
"MSG": "Nu ai ținut evidența timpului pentru {{time}}",
"MSG_WITHOUT_TIME": "Nu ai ținut evidența timpului"
},
"D_ARCHIVE_COMPRESS": {
"BTN_COMPRESS": "Comprimă arhiva",
@ -1928,10 +1952,10 @@
"DO_IT": "Fă-l!",
"DONT_SHOW_AGAIN": "Nu mai afișa din nou",
"DUPLICATE": "Duplică",
"DURATION_DESCRIPTION": "ex: \"5h 23m\" care rezultă în 5 ore și 23 minute",
"DURATION_DESCRIPTION": "d.e. \"5h 23m\" care rezultă în 5 ore și 23 minute",
"EDIT": "Editează",
"ENABLED": "Activat",
"EXAMPLE_VAL": "ex: 32m",
"EXAMPLE_VAL": "d.e. 32m",
"EXTENSION_INFO": "Te rog <a target=\"_blank\" href=\"https://chrome.google.com/webstore/detail/super-productivity/ljkbjodfmekklcoibdnhahlaalhihmlb\"> descarcă extensia Chrome</a> pentru a permite comunicarea cu API-ul Jira și tratarea timpului mort. Acest lucru nu funcționează pe mobil. <strong>Fără extensie, aceste funcții nu vor funcționa!</strong>",
"HIDE": "Ascunde",
"ICON_INP_DESCRIPTION": "Toate emoji-urile UTF-8 sunt de asemenea suportate!",
@ -1942,7 +1966,7 @@
"NEXT": "Următorul",
"NO_CON": "Momentan ești offline. Te rog reconectează-te la internet.",
"NONE": "Niciunul",
"OK": "Ok",
"OK": "OK",
"OPEN_IN_BROWSER": "Deschide în navigatorul web",
"OVERDUE": "Restante",
"PASSWORD_STRENGTH_FAIR": "Acceptabilă",
@ -1962,25 +1986,27 @@
},
"GCF": {
"APP_FEATURES": {
"BOARDS": "Boards",
"DONATE_PAGE": "Donate page",
"BOARDS": "Table",
"DONATE_PAGE": "Pagină de donații",
"FINISH_DAY": "Încheie ziua",
"FOCUS_MODE": "Focus mode",
"FOCUS_MODE": "Mod concentrare",
"HABITS": "Obiceiuri",
"HELP": "Enable or disable specific app features across the UI.",
"ISSUES_PANEL": "Issues panel",
"PLANNER": "Planner",
"PROJECT_NOTES": "Project notes",
"HELP": "Activează sau dezactivează anumite funcții ale aplicației din interfață.",
"ISSUES_PANEL": "Panou probleme",
"PLANNER": "Planificator",
"PROJECT_NOTES": "Notițe proiect",
"SCHEDULE": "Orar",
"SCHEDULE_DAY_PANEL": "Schedule day panel",
"SCHEDULE_DAY_PANEL": "Panou programare zi",
"SEARCH": "Caută",
"SYNC_BUTTON": "Sync button",
"TIME_TRACKING": "Stopwatch time tracking",
"TITLE": "App Features",
"USER_PROFILES": "User profiles (experimental)",
"USER_PROFILES_HINT": "Allows you to create and switch between different user profiles, each with separate settings, tasks, and sync configurations. The profile management button will appear in the top-right corner when enabled. Note: Disabling this feature will hide the UI but preserve your profile data. (Experimental feature. No guarantees. Make sure to have a backup!)",
"USER_PROFILES_WARNING": "<strong>Funcție experimentală:</strong> Această funcție este încă în dezvoltare și poate fi eliminată sau modificată semnificativ într-o actualizare viitoare. Folosește pe propriul risc și asigură-te că ai backup-uri ale datelor."
},
"SYNC_BUTTON": "Buton sincronizare",
"TIME_TRACKING": "Cronometru urmărire timp",
"TITLE": "Funcții aplicație",
"USER_PROFILES": "Profiluri utilizator (experimental — ar putea fi eliminate)",
"USER_PROFILES_HINT": "Îți permite să creezi și să comuti între diferite profiluri de utilizator, fiecare cu setări, sarcini și configurații de sincronizare separate. Butonul de gestionare a profilurilor va apărea în colțul din dreapta sus când este activat. Notă: Dezactivarea acestei funcții va ascunde interfața dar va păstra datele profilului. (Funcție experimentală. Fără garanții. Asigură-te că ai o copie de rezervă!)",
"USER_PROFILES_WARNING": "<strong>Funcție experimentală:</strong> Această funcție este încă în dezvoltare și ar putea fi eliminată sau modificată semnificativ într-o actualizare viitoare. Folosește cu grijă și asigură-te că ai copii de rezervă ale datelor tale.",
"EXPERIMENTAL_WARNING_TITLE": "Feature Experimental",
"EXPERIMENTAL_WARNING_MSG": "Această funcție este experimentală. Poate conține erori, poate fi modificată semnificativ sau poate fi eliminată într-o actualizare viitoare. Asigură-te că ai copii de rezervă ale datelor tale.<br/><br/>Vrei să o activezi?"
},
"AUTO_BACKUPS": {
"HELP": "Automatically save all data to your app folder to have it ready if something goes wrong.",
"LABEL_IS_ENABLED": "Enable automatic backups",
@ -1995,8 +2021,8 @@
"SHOW_BANNER_THRESHOLD": "Show a notification X before the event (blank for disabled)"
},
"CLIPBOARD_IMAGES": {
"DELETE": "Delete image",
"DELETE_ALL": "Delete All",
"DELETE": "Șterge imaginea",
"DELETE_ALL": "Șterge-le pe toate",
"DELETE_ALL_SUCCESS": "Toate imaginile din clipboard șterse cu succes",
"DELETE_SUCCESS": "Imagine din clipboard ștearsă cu succes",
"ERROR_DELETING": "Ștergerea imaginii din clipboard a eșuat",
@ -2013,14 +2039,14 @@
"PATH_LABEL": "Cale stocare imagini",
"PATH_PLACEHOLDER": "Lasă gol pentru locația implicită",
"PATH_TITLE": "Locație stocare",
"SELECT_PATH": "Select Folder",
"SELECT_PATH": "Selectează dosarul",
"SELECT_PATH_TITLE": "Selectează dosarul pentru imagini clipboard",
"SORT_BY": "Sort by",
"SORT_LARGEST": "Largest first",
"SORT_NEWEST": "Newest first",
"SORT_OLDEST": "Oldest first",
"SORT_SMALLEST": "Smallest first",
"TITLE": "Imagini clipboard",
"SORT_BY": "Sortează după",
"SORT_LARGEST": "Cele mai mari primele",
"SORT_NEWEST": "Cele mai noi primele",
"SORT_OLDEST": "Cele mai vechi primele",
"SORT_SMALLEST": "Cele mai mici primele",
"TITLE": "Imagini Clipboard",
"TOTAL_IMAGES": "Total imagini",
"TOTAL_SIZE": "Dimensiune totală"
},
@ -2149,6 +2175,7 @@
"TIME_LOCALE_IT_IT": "Italienesc: 24 de ore, DD/MM/YYYY",
"TIME_LOCALE_JA_JP": "Japonez: 24 de ore, YYYY/MM/DD",
"TIME_LOCALE_KO_KR": "Korean: 12 ore AM/PM, YYYY. MM. DD",
"TIME_LOCALE_PL_PL": "Polonez: 24 de ore, DD.MM.YYYY",
"TIME_LOCALE_PT_BR": "Portughez (Brazilia): 24 de ore, DD/MM/YYYY",
"TIME_LOCALE_RO_MD": "Românesc (Moldova): 24 de ore, DD.MM.YYYY",
"TIME_LOCALE_RO_RO": "Românesc: 24 de ore, DD.MM.YYYY",
@ -2218,10 +2245,10 @@
"L_LUNCH_BREAK_START": "Lunch break start",
"L_WORK_END": "Work Day End",
"L_WORK_START": "Work Day Start",
"LUNCH_BREAK_START_END_DESCRIPTION": "ex: 13:00",
"LUNCH_BREAK_START_END_DESCRIPTION": "d.e. 13:00",
"MONTH": "Lună",
"TITLE": "Programare",
"WORK_START_END_DESCRIPTION": "ex: 17:00"
"WORK_START_END_DESCRIPTION": "d.e. 17:00"
},
"SHORT_SYNTAX": {
"HELP": "<p>Here you can control short syntax options when creating a task</p>",
@ -2235,9 +2262,9 @@
"URL_BEHAVIOR_LABEL": "URL behavior when adding tasks"
},
"SOUND": {
"BREAK_REMINDER_SOUND": "Take a break reminder sound",
"DONE_SOUND": "Task completed sound",
"IS_INCREASE_DONE_PITCH": "Increase pitch for every task completed",
"BREAK_REMINDER_SOUND": "Sunet memento de luat o pauză",
"DONE_SOUND": "Sunet sarcină finalizată",
"IS_INCREASE_DONE_PITCH": "Crește tonul pentru fiecare sarcină finalizată",
"TITLE": "Sunet",
"TRACK_TIME_SOUND": "Sunet memento de urmărire a timpului",
"VOLUME": "Volum"
@ -2646,18 +2673,19 @@
"CREATE_NEW_PROFILE": "Creează un nou profil",
"CREATED": "Creat:",
"DELETE_PROFILE": "Șterge profilul",
"DEPRECATION_WARNING": "Funcția experimentală Profile utilizator va fi eliminată într-o actualizare viitoare. Te rog să îți exporti datele de profil ca backup.",
"DIALOG_TITLE": "Administrează profilurile de utilizator",
"EXISTING_PROFILES": "Profiluri existente",
"EXPORT_PROFILE": "Exportă profilul",
"MANAGE_PROFILES": "Gestionează profilurile...",
"PROFILE_NAME": "Nume profil",
"PROFILE_NAME_PLACEHOLDER": "ex: Muncă, Personal",
"PROFILE_NAME_PLACEHOLDER": "d.e. Muncă, Personal",
"RENAME": "Redenumește",
"SAVE": "Salvează"
},
"V": {
"E_DATETIME": "Valoare introdusă nu este o dată și oră!",
"E_DURATION": "Te rog introdu o durată validă (ex: 1h)",
"E_DURATION": "Te rog introdu o durată validă (d.e. 1h)",
"E_MAX": "Nu trebuie să fie mai mare decât {{val}}",
"E_MAX_LENGTH": "Trebuie să aibă cel mult {{val}} caractere",
"E_MIN": "Nu trebuie să fie mai mică decât {{val}}",