mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
feat(sync): nudge long-time users without sync to set it up (#8637)
* feat(sync): nudge long-time users without sync to set it up Offline-first means a user who never configures sync has no backup at all; clearing browser data or losing the device wipes everything. Once the app has clearly been used for a while AND holds real data, show a calm, low-priority banner once encouraging sync setup. Trigger combines wall-clock age (>= 7 days since a lazily-seeded FIRST_USE_TIMESTAMP) with a task-count gate (>= 20 tasks), so it is robust both to users who restart many times a day and to those who leave the app running for weeks (where app-start count fails), and never nags an empty/dormant install. Shown at most once: both "Set up sync" and "Not now" persist a dismissed flag; a configured provider suppresses it entirely. Reuses the existing banner system and mirrors NoteStartupBannerService. * refactor(sync): apply review feedback to data-safety nudge - Rename LS.FIRST_USE_TIMESTAMP -> SYNC_SAFETY_FIRST_SEEN and document that it is not a true install date (seeded at upgrade time for existing installs), so no other feature reuses it as one. - Tests: add an unhydrated-config (undefined) skip case and assert the task-count selector, guarding the earlier race fix and a selector swap. - i18n: match each locale's existing register for the nudge message (pt/cs/sk/tr -> formal, id -> informal) per translation review. * test(planner): stabilize add-subtask-from-detail focus race Wait for the detail panel's deferred open-time auto-focus to settle before opening the inline subtask draft. The auto-focus lands ~200ms after the panel opens and, if it fires after the draft input is focused, steals focus and blur-closes the input — making toBeFocused() flake with 'element(s) not found'. Mirrors the guard already used in add-subtask-with-detail-panel-open.spec.ts.
This commit is contained in:
parent
06a7425698
commit
a1c059855e
35 changed files with 447 additions and 0 deletions
|
|
@ -34,6 +34,17 @@ test.describe('Planner: add subtask from detail panel', () => {
|
|||
const panel = page.locator(DETAIL_PANEL);
|
||||
await expect(panel).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Let the panel's deferred open-time auto-focus land first. It fires ~200ms
|
||||
// after open (delay(50) + a 150ms guarded focus) and moves focus to the
|
||||
// first detail item; if it lands *after* we open the inline draft below it
|
||||
// steals focus from the input, blur-closing it — the input then vanishes
|
||||
// mid-test and `toBeFocused()` fails with "element(s) not found".
|
||||
await expect
|
||||
.poll(async () =>
|
||||
page.evaluate(() => !!document.activeElement?.closest('task-detail-panel')),
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
// The sub-task section starts collapsed; expand it to reveal the button.
|
||||
await panel
|
||||
.locator('mat-expansion-panel-header')
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ import { TaskService } from './features/tasks/task.service';
|
|||
import { MatMenuItem } from '@angular/material/menu';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { NoteStartupBannerService } from './features/note/note-startup-banner.service';
|
||||
import { SyncSafetyBannerService } from './imex/sync/sync-safety-banner.service';
|
||||
import { ProjectService } from './features/project/project.service';
|
||||
import { TagService } from './features/tag/tag.service';
|
||||
import { ContextMenuComponent } from './ui/context-menu/context-menu.component';
|
||||
|
|
@ -154,6 +155,7 @@ export class AppComponent implements OnDestroy, AfterViewInit {
|
|||
private _tagService = inject(TagService);
|
||||
private _destroyRef = inject(DestroyRef);
|
||||
private _noteStartupBannerService = inject(NoteStartupBannerService);
|
||||
private _syncSafetyBannerService = inject(SyncSafetyBannerService);
|
||||
private _ngZone = inject(NgZone);
|
||||
private _document = inject(DOCUMENT, { optional: true });
|
||||
private _startupService = inject(StartupService);
|
||||
|
|
@ -309,6 +311,7 @@ export class AppComponent implements OnDestroy, AfterViewInit {
|
|||
.pipe(take(1))
|
||||
.subscribe(() => {
|
||||
void this._noteStartupBannerService.showLastNoteIfNeeded();
|
||||
this._syncSafetyBannerService.showReminderIfNeeded();
|
||||
});
|
||||
|
||||
// ! For keyboard shortcuts to work correctly with any layouts (QWERTZ/AZERTY/etc) - user's keyboard layout must be presaved
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export enum BannerId {
|
|||
FocusModeSessionDone = 'FocusModeSessionDone',
|
||||
StartupNote = 'StartupNote',
|
||||
DeadlinesToday = 'DeadlinesToday',
|
||||
SyncSafetyReminder = 'SyncSafetyReminder',
|
||||
}
|
||||
|
||||
export const BANNER_SORT_PRIO_MAP = {
|
||||
|
|
@ -28,6 +29,7 @@ export const BANNER_SORT_PRIO_MAP = {
|
|||
[BannerId.StartupNote]: 2,
|
||||
[BannerId.Offline]: 0,
|
||||
[BannerId.InstallWebApp]: 0,
|
||||
[BannerId.SyncSafetyReminder]: 0,
|
||||
} as const;
|
||||
|
||||
export interface BannerAction {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ export enum DB {
|
|||
export enum LS {
|
||||
APP_START_COUNT = 'APP_START_COUNT',
|
||||
APP_START_COUNT_LAST_START_DAY = 'APP_START_COUNT_LAST_START_DAY',
|
||||
// Epoch ms first observed by SyncSafetyBannerService (seeded once, lazily, on
|
||||
// its first run). Used only to tell "used for a while" by wall-clock time for
|
||||
// the sync-setup nudge. NOT a true install date: for installs that predate
|
||||
// this feature it is seeded at upgrade time, so don't reuse it as one.
|
||||
SYNC_SAFETY_FIRST_SEEN = 'SUP_SYNC_SAFETY_FIRST_SEEN',
|
||||
RATE_DIALOG_STATE = 'SUP_RATE_DIALOG_STATE',
|
||||
// Set on an unhandled error or any detected data damage; read by the rating
|
||||
// prompt to hold off for a cooldown after a bad experience. Time only.
|
||||
|
|
@ -33,6 +38,10 @@ export enum LS {
|
|||
|
||||
LAST_NOTE_BANNER_DAY = 'SUP_LAST_NOTE_BANNER_DAY',
|
||||
|
||||
// Set once the user acts on or dismisses the "set up sync to keep your data
|
||||
// safe" startup banner, so the nudge is shown at most once ever.
|
||||
SYNC_SAFETY_NUDGE_DISMISSED = 'SUP_SYNC_SAFETY_NUDGE_DISMISSED',
|
||||
|
||||
SELECTED_TIME_VIEW = 'SELECTED_TIME_VIEW',
|
||||
SCHEDULE_WEEK_ROW_HEIGHT = 'SUP_SCHEDULE_WEEK_ROW_HEIGHT',
|
||||
|
||||
|
|
|
|||
169
src/app/imex/sync/sync-safety-banner.service.spec.ts
Normal file
169
src/app/imex/sync/sync-safety-banner.service.spec.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { SyncSafetyBannerService } from './sync-safety-banner.service';
|
||||
import { BannerService } from '../../core/banner/banner.service';
|
||||
import { BannerId, Banner } from '../../core/banner/banner.model';
|
||||
import { LS } from '../../core/persistence/storage-keys.const';
|
||||
import { GlobalConfigService } from '../../features/config/global-config.service';
|
||||
import { OnboardingHintService } from '../../features/onboarding/onboarding-hint.service';
|
||||
import { SyncConfig } from '../../features/config/global-config.model';
|
||||
import { SyncProviderId } from '../../op-log/sync-providers/provider.const';
|
||||
import { selectTaskFeatureState } from '../../features/tasks/store/task.selectors';
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const NOW = 1_700_000_000_000;
|
||||
const UNCONFIGURED: Partial<SyncConfig> = { isEnabled: false, syncProvider: null };
|
||||
const CONFIGURED: Partial<SyncConfig> = {
|
||||
isEnabled: true,
|
||||
syncProvider: SyncProviderId.Dropbox,
|
||||
};
|
||||
|
||||
describe('SyncSafetyBannerService', () => {
|
||||
let service: SyncSafetyBannerService;
|
||||
let bannerService: jasmine.SpyObj<BannerService>;
|
||||
let matDialog: jasmine.SpyObj<MatDialog>;
|
||||
let syncSpy: jasmine.Spy;
|
||||
let selectSignalSpy: jasmine.Spy;
|
||||
let lsStore: Record<string, string>;
|
||||
let taskIds: string[];
|
||||
|
||||
const lastBanner = (): Banner =>
|
||||
bannerService.open.calls.mostRecent().args[0] as Banner;
|
||||
|
||||
const daysAgoTs = (n: number): string => {
|
||||
const offset = n * DAY_MS;
|
||||
return (NOW - offset).toString();
|
||||
};
|
||||
const manyTasks = (): string[] => Array.from({ length: 30 }, (_, i) => `t${i}`);
|
||||
|
||||
// Defaults to a clearly-eligible user: first used 8 days ago, 30 tasks.
|
||||
const setEligible = (): void => {
|
||||
lsStore[LS.SYNC_SAFETY_FIRST_SEEN] = daysAgoTs(8);
|
||||
taskIds = manyTasks();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
lsStore = {};
|
||||
taskIds = [];
|
||||
spyOn(Date, 'now').and.returnValue(NOW);
|
||||
spyOn(localStorage, 'getItem').and.callFake((k) => lsStore[k] ?? null);
|
||||
spyOn(localStorage, 'setItem').and.callFake((k, v) => {
|
||||
lsStore[k] = v;
|
||||
});
|
||||
spyOn(OnboardingHintService, 'isOnboardingInProgress').and.returnValue(false);
|
||||
|
||||
bannerService = jasmine.createSpyObj<BannerService>('BannerService', ['open']);
|
||||
matDialog = jasmine.createSpyObj<MatDialog>('MatDialog', ['open']);
|
||||
syncSpy = jasmine.createSpy('sync').and.returnValue(UNCONFIGURED);
|
||||
// Reads live `taskIds` so each test can vary the count before invoking.
|
||||
selectSignalSpy = jasmine
|
||||
.createSpy('selectSignal')
|
||||
.and.callFake(() => () => ({ ids: taskIds }));
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
SyncSafetyBannerService,
|
||||
{ provide: BannerService, useValue: bannerService },
|
||||
{ provide: MatDialog, useValue: matDialog },
|
||||
{ provide: GlobalConfigService, useValue: { sync: syncSpy } },
|
||||
{ provide: Store, useValue: { selectSignal: selectSignalSpy } },
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(SyncSafetyBannerService);
|
||||
});
|
||||
|
||||
it('shows the banner once used for a while with real data and no sync', () => {
|
||||
setEligible();
|
||||
service.showReminderIfNeeded();
|
||||
expect(bannerService.open).toHaveBeenCalledTimes(1);
|
||||
expect(lastBanner().id).toBe(BannerId.SyncSafetyReminder);
|
||||
// Guards against accidentally swapping in a different task selector.
|
||||
expect(selectSignalSpy).toHaveBeenCalledWith(selectTaskFeatureState);
|
||||
});
|
||||
|
||||
it('does not show while sync config is still unhydrated (undefined)', () => {
|
||||
setEligible();
|
||||
syncSpy.and.returnValue(undefined);
|
||||
service.showReminderIfNeeded();
|
||||
expect(bannerService.open).not.toHaveBeenCalled();
|
||||
// ...but the first-use clock is still seeded for next time.
|
||||
expect(lsStore[LS.SYNC_SAFETY_FIRST_SEEN]).toBe(daysAgoTs(8));
|
||||
});
|
||||
|
||||
it('shows exactly at the thresholds (7 days, 20 tasks)', () => {
|
||||
lsStore[LS.SYNC_SAFETY_FIRST_SEEN] = daysAgoTs(7);
|
||||
taskIds = Array.from({ length: 20 }, (_, i) => `t${i}`);
|
||||
service.showReminderIfNeeded();
|
||||
expect(bannerService.open).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('seeds the first-use timestamp on first run and does not show yet', () => {
|
||||
taskIds = manyTasks();
|
||||
service.showReminderIfNeeded();
|
||||
expect(lsStore[LS.SYNC_SAFETY_FIRST_SEEN]).toBe(NOW.toString());
|
||||
expect(bannerService.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('still seeds the first-use clock during onboarding (without showing)', () => {
|
||||
(OnboardingHintService.isOnboardingInProgress as jasmine.Spy).and.returnValue(true);
|
||||
taskIds = manyTasks();
|
||||
service.showReminderIfNeeded();
|
||||
expect(lsStore[LS.SYNC_SAFETY_FIRST_SEEN]).toBe(NOW.toString());
|
||||
expect(bannerService.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not show before the time threshold even with lots of data', () => {
|
||||
lsStore[LS.SYNC_SAFETY_FIRST_SEEN] = daysAgoTs(3);
|
||||
taskIds = manyTasks();
|
||||
service.showReminderIfNeeded();
|
||||
expect(bannerService.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not show with too little data even after the time threshold', () => {
|
||||
lsStore[LS.SYNC_SAFETY_FIRST_SEEN] = daysAgoTs(8);
|
||||
taskIds = ['a', 'b', 'c', 'd']; // example-task-sized
|
||||
service.showReminderIfNeeded();
|
||||
expect(bannerService.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not show again once dismissed', () => {
|
||||
setEligible();
|
||||
lsStore[LS.SYNC_SAFETY_NUDGE_DISMISSED] = 'true';
|
||||
service.showReminderIfNeeded();
|
||||
expect(bannerService.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not interrupt onboarding', () => {
|
||||
setEligible();
|
||||
(OnboardingHintService.isOnboardingInProgress as jasmine.Spy).and.returnValue(true);
|
||||
service.showReminderIfNeeded();
|
||||
expect(bannerService.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not show when sync is already configured', () => {
|
||||
setEligible();
|
||||
syncSpy.and.returnValue(CONFIGURED);
|
||||
service.showReminderIfNeeded();
|
||||
expect(bannerService.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('persists dismissal when the user clicks "Not now"', () => {
|
||||
setEligible();
|
||||
service.showReminderIfNeeded();
|
||||
lastBanner().action2!.fn();
|
||||
expect(lsStore[LS.SYNC_SAFETY_NUDGE_DISMISSED]).toBe('true');
|
||||
});
|
||||
|
||||
it('persists dismissal and opens the sync dialog on "Set up sync"', () => {
|
||||
const openDialogSpy = spyOn(
|
||||
service as unknown as { _openSyncCfgDialog: () => Promise<void> },
|
||||
'_openSyncCfgDialog',
|
||||
).and.resolveTo();
|
||||
setEligible();
|
||||
service.showReminderIfNeeded();
|
||||
lastBanner().action!.fn();
|
||||
expect(lsStore[LS.SYNC_SAFETY_NUDGE_DISMISSED]).toBe('true');
|
||||
expect(openDialogSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
108
src/app/imex/sync/sync-safety-banner.service.ts
Normal file
108
src/app/imex/sync/sync-safety-banner.service.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { inject, Injectable } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { BannerService } from '../../core/banner/banner.service';
|
||||
import { BannerId } from '../../core/banner/banner.model';
|
||||
import { LS } from '../../core/persistence/storage-keys.const';
|
||||
import { GlobalConfigService } from '../../features/config/global-config.service';
|
||||
import { OnboardingHintService } from '../../features/onboarding/onboarding-hint.service';
|
||||
import { selectTaskFeatureState } from '../../features/tasks/store/task.selectors';
|
||||
import { devError } from '../../util/dev-error';
|
||||
import { T } from '../../t.const';
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
// "Used for a while": wall-clock days since the app was first opened on this
|
||||
// device. Wall-clock (not app-start count) so it is robust both to users who
|
||||
// restart many times a day and to those who leave the app running for weeks.
|
||||
const MIN_DAYS_SINCE_FIRST_USE = 7;
|
||||
|
||||
// ...and there must be a non-trivial amount of data actually worth protecting:
|
||||
// a fresh install seeds only 4 example tasks, so this keeps us from nudging an
|
||||
// empty/dormant app even after the time threshold passes.
|
||||
const MIN_TASKS = 20;
|
||||
|
||||
/**
|
||||
* Offline-first means a user who never configures sync has no backup at all.
|
||||
* Once they have clearly used the app for a while AND accumulated real data,
|
||||
* gently (and exactly once) encourage setting up sync so their data stays safe.
|
||||
* Mirrors the calm, gated-once pattern of NoteStartupBannerService.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SyncSafetyBannerService {
|
||||
private readonly _bannerService = inject(BannerService);
|
||||
private readonly _globalConfigService = inject(GlobalConfigService);
|
||||
private readonly _matDialog = inject(MatDialog);
|
||||
private readonly _store = inject(Store);
|
||||
|
||||
private readonly _taskState = this._store.selectSignal(selectTaskFeatureState);
|
||||
|
||||
showReminderIfNeeded(): void {
|
||||
// Once-ever guard: already acted on / dismissed.
|
||||
if (localStorage.getItem(LS.SYNC_SAFETY_NUDGE_DISMISSED)) {
|
||||
return;
|
||||
}
|
||||
// Start (or read) the first-use clock. Seeding here means it begins ticking
|
||||
// the first time the app boots on this device; an install that already
|
||||
// exists when this feature ships starts its clock now (a deliberate grace
|
||||
// period rather than nudging every existing user at once).
|
||||
const firstUseMs = this._getOrSeedFirstUse();
|
||||
|
||||
// Don't interrupt the first-run onboarding flow.
|
||||
if (OnboardingHintService.isOnboardingInProgress()) {
|
||||
return;
|
||||
}
|
||||
// Already using sync → nothing to nudge. Treat "config not loaded yet"
|
||||
// (undefined) as a skip too, so a synced user is never nudged in a race.
|
||||
const sync = this._globalConfigService.sync();
|
||||
if (!sync || (sync.isEnabled && sync.syncProvider)) {
|
||||
return;
|
||||
}
|
||||
// Used for a while?
|
||||
if (Date.now() - firstUseMs < MIN_DAYS_SINCE_FIRST_USE * DAY_MS) {
|
||||
return;
|
||||
}
|
||||
// Enough data to be worth protecting?
|
||||
if ((this._taskState().ids?.length ?? 0) < MIN_TASKS) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._bannerService.open({
|
||||
id: BannerId.SyncSafetyReminder,
|
||||
msg: T.APP.B_SYNC_SAFETY.MSG,
|
||||
ico: 'cloud_off',
|
||||
action: {
|
||||
label: T.APP.B_SYNC_SAFETY.SETUP,
|
||||
fn: () => {
|
||||
this._dismissForever();
|
||||
void this._openSyncCfgDialog().catch(devError);
|
||||
},
|
||||
},
|
||||
action2: {
|
||||
label: T.APP.B_SYNC_SAFETY.DISMISS,
|
||||
fn: () => this._dismissForever(),
|
||||
},
|
||||
isHideDismissBtn: true,
|
||||
});
|
||||
}
|
||||
|
||||
private _getOrSeedFirstUse(): number {
|
||||
const stored = +(localStorage.getItem(LS.SYNC_SAFETY_FIRST_SEEN) || 0);
|
||||
if (stored) {
|
||||
return stored;
|
||||
}
|
||||
const now = Date.now();
|
||||
localStorage.setItem(LS.SYNC_SAFETY_FIRST_SEEN, now.toString());
|
||||
return now;
|
||||
}
|
||||
|
||||
private _dismissForever(): void {
|
||||
localStorage.setItem(LS.SYNC_SAFETY_NUDGE_DISMISSED, 'true');
|
||||
}
|
||||
|
||||
private async _openSyncCfgDialog(): Promise<void> {
|
||||
const { DialogSyncCfgComponent } =
|
||||
await import('./dialog-sync-cfg/dialog-sync-cfg.component');
|
||||
this._matDialog.open(DialogSyncCfgComponent);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,11 @@ const T = {
|
|||
MSG: 'APP.B_INSTALL.MSG',
|
||||
},
|
||||
B_OFFLINE: 'APP.B_OFFLINE',
|
||||
B_SYNC_SAFETY: {
|
||||
DISMISS: 'APP.B_SYNC_SAFETY.DISMISS',
|
||||
MSG: 'APP.B_SYNC_SAFETY.MSG',
|
||||
SETUP: 'APP.B_SYNC_SAFETY.SETUP',
|
||||
},
|
||||
CONTEXT_MENU: {
|
||||
CHANGE_PROJECT_SETTINGS: 'APP.CONTEXT_MENU.CHANGE_PROJECT_SETTINGS',
|
||||
CHANGE_TAG_SETTINGS: 'APP.CONTEXT_MENU.CHANGE_TAG_SETTINGS',
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "هل تريد تثبيت الإنتاجية الفائقة كتطبيق ويب؟"
|
||||
},
|
||||
"B_OFFLINE": "أنت غير متصل بالإنترنت. لن تعمل مزامنة بيانات موفر المشكلة وطلبها.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "ليس الآن",
|
||||
"MSG": "أنت تستخدم Super Productivity منذ فترة. قم بإعداد المزامنة لعمل نسخة احتياطية من بياناتك والحفاظ عليها آمنة عبر جميع أجهزتك.",
|
||||
"SETUP": "إعداد المزامنة"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "تغيير الخلفية",
|
||||
"CHANGE_PROJECT_SETTINGS": "تغيير إعدادات المشروع",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Chcete nainstalovat Super Produktivitu jako PWA?"
|
||||
},
|
||||
"B_OFFLINE": "Jste odpojeni od internetu. Synchronizace a požadavky na data poskytovatele problémů nebudou fungovat.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Teď ne",
|
||||
"MSG": "Super Productivity používáte už nějakou dobu. Nastavte synchronizaci, abyste zálohovali svá data a udrželi je v bezpečí na všech zařízeních.",
|
||||
"SETUP": "Nastavit synchronizaci"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "Změnit pozadí"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Möchtest du Super Productivity als PWA installieren?"
|
||||
},
|
||||
"B_OFFLINE": "Du bist vom Internet getrennt. Das Synchronisieren und Anfordern von Daten des Issue-Providers funktioniert nicht.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Jetzt nicht",
|
||||
"MSG": "Du nutzt Super Productivity schon eine Weile. Richte die Synchronisierung ein, um deine Daten zu sichern und geräteübergreifend zu schützen.",
|
||||
"SETUP": "Synchronisierung einrichten"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_PROJECT_SETTINGS": "Projekteinstellungen ändern",
|
||||
"CHANGE_TAG_SETTINGS": "Tag-Einstellungen ändern"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Do you want to install Super Productivity as a PWA (Progressive Web App)?"
|
||||
},
|
||||
"B_OFFLINE": "You are disconnected from the internet. Syncing and requesting issue provider data will not work.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Not now",
|
||||
"MSG": "You've been using Super Productivity for a while. Set up sync to back up your data and keep it safe across devices.",
|
||||
"SETUP": "Set up sync"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_PROJECT_SETTINGS": "Change Project Settings",
|
||||
"CHANGE_TAG_SETTINGS": "Change Tag Settings"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "¿Quieres instalar Super Productivity como una PWA?"
|
||||
},
|
||||
"B_OFFLINE": "Estás desconectado de internet. La sincronización y la solicitud de datos al proveedor de incidencias no funcionarán.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Ahora no",
|
||||
"MSG": "Llevas un tiempo usando Super Productivity. Configura la sincronización para hacer una copia de seguridad de tus datos y mantenerlos seguros en todos tus dispositivos.",
|
||||
"SETUP": "Configurar sincronización"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "Cambiar fondo",
|
||||
"CHANGE_PROJECT_SETTINGS": "Cambiar configuración del proyecto",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "آیا میخواهید سوپر پروداکتیویتی به عنوان PWA نصب بشود؟"
|
||||
},
|
||||
"B_OFFLINE": "شما از اینترنت جدا هستید. همگام سازی و درخواست داده های ارائه دهنده مشکل کار نمی کند.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "اکنون نه",
|
||||
"MSG": "مدتی است که از Super Productivity استفاده میکنید. همگامسازی را تنظیم کنید تا از دادههای خود نسخه پشتیبان تهیه کرده و آنها را در همه دستگاهها ایمن نگه دارید.",
|
||||
"SETUP": "تنظیم همگامسازی"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "تغییر پیش زمینه"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Haluatko asentaa Super Productivityn PWA-sovellukseksi?"
|
||||
},
|
||||
"B_OFFLINE": "Olet irti internetistä. Synkronointi ja tiedustelut ongelmanantajien tiedoista eivät toimi.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Ei nyt",
|
||||
"MSG": "Olet käyttänyt Super Productivitya jo jonkin aikaa. Määritä synkronointi varmuuskopioidaksesi tietosi ja pitääksesi ne turvassa kaikilla laitteilla.",
|
||||
"SETUP": "Määritä synkronointi"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "Vaihda taustakuva"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Voulez-vous installer Super Productivity en tant que PWA ?"
|
||||
},
|
||||
"B_OFFLINE": "Vous êtes déconnecté d'Internet. La synchronisation et la demande des données du fournisseur de problèmes ne fonctionneront pas.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Pas maintenant",
|
||||
"MSG": "Vous utilisez Super Productivity depuis un certain temps. Configurez la synchronisation pour sauvegarder vos données et les garder en sécurité sur tous vos appareils.",
|
||||
"SETUP": "Configurer la synchronisation"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "Changer le fond",
|
||||
"CHANGE_PROJECT_SETTINGS": "Modifier les paramètres du projet",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Želiš instalirati Super Productivity kao PWA (Progressive Web App)?"
|
||||
},
|
||||
"B_OFFLINE": "Nemaš vezu s internetom. Sinkronizacija i dohvaćanje podataka od usluge za praćenje problema neće raditi.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Ne sada",
|
||||
"MSG": "Već neko vrijeme koristiš Super Productivity. Postavi sinkronizaciju kako bi sigurnosno kopirao svoje podatke i čuvao ih sigurnima na svim uređajima.",
|
||||
"SETUP": "Postavi sinkronizaciju"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_PROJECT_SETTINGS": "Promijeni postavke projekta",
|
||||
"CHANGE_TAG_SETTINGS": "Promijeni postavke oznaka"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Apa kamu ingin memasang Super Productivity sebagai sebuah PWA?"
|
||||
},
|
||||
"B_OFFLINE": "Jaringanmu terputus dari internet. Sinkronasi dan meminta masalah penyedia data mungkin tidak akan bekerja.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Nanti saja",
|
||||
"MSG": "Kamu sudah menggunakan Super Productivity beberapa waktu. Siapkan sinkronisasi untuk mencadangkan datamu dan menjaganya tetap aman di semua perangkat.",
|
||||
"SETUP": "Siapkan sinkronisasi"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "Ubah Latar Belakang",
|
||||
"CHANGE_PROJECT_SETTINGS": "Ubah pengaturan proyek",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Vuoi installare Super Productivity come PWA?"
|
||||
},
|
||||
"B_OFFLINE": "Sei disconnesso da Internet. La sincronizzazione e la richiesta dei dati del provider di problemi non funzioneranno.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Non ora",
|
||||
"MSG": "Usi Super Productivity da un po'. Configura la sincronizzazione per eseguire il backup dei tuoi dati e tenerli al sicuro su tutti i dispositivi.",
|
||||
"SETUP": "Configura sincronizzazione"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "Cambia sfondo",
|
||||
"CHANGE_PROJECT_SETTINGS": "Modifica impostazioni progetto",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Super ProductivityをPWAとしてインストールしますか?"
|
||||
},
|
||||
"B_OFFLINE": "インターネットに接続していません。課題プロバイダーデータの同期と要求は機能しません。",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "後で",
|
||||
"MSG": "Super Productivity をしばらくご利用いただいています。同期を設定して、データをバックアップし、すべてのデバイスで安全に保ちましょう。",
|
||||
"SETUP": "同期を設定"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "変更の背景",
|
||||
"CHANGE_PROJECT_SETTINGS": "プロジェクト設定を変更",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "PWA로 Super Productivity를 설치하시겠습니까?"
|
||||
},
|
||||
"B_OFFLINE": "인터넷 연결이 끊어졌습니다. 문제 제공자 데이터 동기화 및 요청이 작동하지 않습니다.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "나중에",
|
||||
"MSG": "Super Productivity를 사용하신 지 꽤 되셨네요. 동기화를 설정하여 데이터를 백업하고 모든 기기에서 안전하게 보관하세요.",
|
||||
"SETUP": "동기화 설정"
|
||||
},
|
||||
"UPDATE_WEB_APP": "새 버전을 사용할 수 있습니다. 새 버전을 로드하시겠습니까?",
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "배경 변경",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Vil du installere Super Productivity som en PWA?"
|
||||
},
|
||||
"B_OFFLINE": "Du er koblet fra internett. Synkronisering og å be om data fra leverandører fungerer ikke.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Ikke nå",
|
||||
"MSG": "Du har brukt Super Productivity en stund. Sett opp synkronisering for å sikkerhetskopiere dataene dine og holde dem trygge på tvers av enheter.",
|
||||
"SETUP": "Sett opp synkronisering"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "Endre bakgrunn"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Wilt u Super Productivity als PWA installeren?"
|
||||
},
|
||||
"B_OFFLINE": "Je hebt geen verbinding met internet. Het synchroniseren en opvragen van probleemprovidergegevens werkt niet.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Niet nu",
|
||||
"MSG": "Je gebruikt Super Productivity al een tijdje. Stel synchronisatie in om een back-up van je gegevens te maken en ze veilig te houden op al je apparaten.",
|
||||
"SETUP": "Synchronisatie instellen"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "Achtergrond wijzigen",
|
||||
"CHANGE_PROJECT_SETTINGS": "Projectinstellingen wijzigen",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Czy chcesz zinstalować Super Productivity jako aplikację PWA?"
|
||||
},
|
||||
"B_OFFLINE": "Zostałeś odłączony od internetu. Synchronizacja nie będzie działać.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Nie teraz",
|
||||
"MSG": "Używasz Super Productivity już od jakiegoś czasu. Skonfiguruj synchronizację, aby utworzyć kopię zapasową danych i zapewnić im bezpieczeństwo na wszystkich urządzeniach.",
|
||||
"SETUP": "Skonfiguruj synchronizację"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_PROJECT_SETTINGS": "Zmień ustawienia Projektu",
|
||||
"CHANGE_TAG_SETTINGS": "Zmień ustawienia Tagów"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Deseja instalar o Super Productivity como um PWA (Progressive Web App)?"
|
||||
},
|
||||
"B_OFFLINE": "Você está desconectado da internet. A sincronização e a solicitação de dados de provedores de issues não funcionarão.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Agora não",
|
||||
"MSG": "Você usa o Super Productivity há algum tempo. Configure a sincronização para fazer backup dos seus dados e mantê-los seguros em todos os dispositivos.",
|
||||
"SETUP": "Configurar sincronização"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_PROJECT_SETTINGS": "Alterar Configurações do Projeto",
|
||||
"CHANGE_TAG_SETTINGS": "Alterar Configurações da Etiqueta"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Deseja instalar Super Productivity como PWA?"
|
||||
},
|
||||
"B_OFFLINE": "Você está desconectado da internet. A sincronização e solicitação de dados do provedor de problemas não funcionará.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Agora não",
|
||||
"MSG": "Já usa o Super Productivity há algum tempo. Configure a sincronização para fazer uma cópia de segurança dos seus dados e mantê-los seguros em todos os dispositivos.",
|
||||
"SETUP": "Configurar sincronização"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "Mudar Fundo",
|
||||
"CHANGE_PROJECT_SETTINGS": "Alterar definições do projeto",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Vrei să instalezi Super Productivity ca PWA (Progressive Web App)?"
|
||||
},
|
||||
"B_OFFLINE": "Ești deconectat de la internet. Sincronizarea și solicitarea datelor furnizorului de probleme nu vor funcționa.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Nu acum",
|
||||
"MSG": "Folosești Super Productivity de ceva timp. Configurează sincronizarea pentru a face o copie de rezervă a datelor tale și a le păstra în siguranță pe toate dispozitivele.",
|
||||
"SETUP": "Configurează sincronizarea"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_PROJECT_SETTINGS": "Schimbă setările proiectului",
|
||||
"CHANGE_TAG_SETTINGS": "Schimbă setările etichetelor"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Vrei să instalezi Super Productivity ca PWA (Progressive Web App)?"
|
||||
},
|
||||
"B_OFFLINE": "Ești deconectat de la internet. Sincronizarea și solicitarea datelor furnizorului de probleme nu vor funcționa.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Nu acum",
|
||||
"MSG": "Folosești Super Productivity de ceva timp. Configurează sincronizarea pentru a face o copie de rezervă a datelor tale și a le păstra în siguranță pe toate dispozitivele.",
|
||||
"SETUP": "Configurează sincronizarea"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_PROJECT_SETTINGS": "Schimbă setările proiectului",
|
||||
"CHANGE_TAG_SETTINGS": "Schimbă setările etichetelor"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Хотите установить Super Productivity в качестве PWA?"
|
||||
},
|
||||
"B_OFFLINE": "Вы отключены от интернета. Синхронизация и запрос данных поставщика проблемы не будут работать.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Не сейчас",
|
||||
"MSG": "Вы пользуетесь Super Productivity уже некоторое время. Настройте синхронизацию, чтобы создать резервную копию данных и сохранить их в безопасности на всех устройствах.",
|
||||
"SETUP": "Настроить синхронизацию"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "Изменить фон",
|
||||
"CHANGE_PROJECT_SETTINGS": "Изменить настройки проекта",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Chcete nainštalovať aplikáciu Super Productivity ako PWA?"
|
||||
},
|
||||
"B_OFFLINE": "Boli ste odpojení od internetu. Synchronizácia a vyžiadanie údajov nebudú fungovať.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Teraz nie",
|
||||
"MSG": "Super Productivity používate už nejaký čas. Nastavte synchronizáciu, aby ste zálohovali svoje údaje a udržali ich v bezpečí na všetkých zariadeniach.",
|
||||
"SETUP": "Nastaviť synchronizáciu"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "Zmeniť pozadie"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Vill du installera Super Productivity som en PWA?"
|
||||
},
|
||||
"B_OFFLINE": "Du är bortkopplad från internet. Synkronisering och begäran om data från ärendeleverantören fungerar inte.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Inte nu",
|
||||
"MSG": "Du har använt Super Productivity ett tag. Konfigurera synkronisering för att säkerhetskopiera dina data och hålla dem säkra på alla enheter.",
|
||||
"SETUP": "Konfigurera synkronisering"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "Ändra bakgrund"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Süper Verimliliği PWA olarak kurmak ister misiniz?"
|
||||
},
|
||||
"B_OFFLINE": "İnternet bağlantınız kesildi. Verileri senkronize etmek ve sağlayıcıdan sorunları istemek işe yaramaz.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Şimdi değil",
|
||||
"MSG": "Super Productivity'yi bir süredir kullanıyorsunuz. Verilerinizi yedeklemek ve tüm cihazlarınızda güvende tutmak için senkronizasyonu ayarlayın.",
|
||||
"SETUP": "Senkronizasyonu ayarla"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_PROJECT_SETTINGS": "Proje Ayarlarını Değiştir",
|
||||
"CHANGE_TAG_SETTINGS": "Etiket Ayarlarını Değiştir"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Бажаєте встановити Super Productivity як PWA?"
|
||||
},
|
||||
"B_OFFLINE": "Ви не підключені до інтернету. Синхронізація та запит даних з провайдера завдань не працюватимуть.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Не зараз",
|
||||
"MSG": "Ви користуєтеся Super Productivity вже певний час. Налаштуйте синхронізацію, щоб створити резервну копію даних і зберегти їх у безпеці на всіх пристроях.",
|
||||
"SETUP": "Налаштувати синхронізацію"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_PROJECT_SETTINGS": "Змінити налаштування проекту",
|
||||
"CHANGE_TAG_SETTINGS": "Змінити налаштування тегів"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "Bạn có muốn cài đặt Super Productivity dưới dạng PWA (Ứng dụng web tiến bộ) không?"
|
||||
},
|
||||
"B_OFFLINE": "Bạn đang ngoại tuyến. Đồng bộ và yêu cầu dữ liệu từ nhà cung cấp sự cố sẽ không hoạt động.",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "Để sau",
|
||||
"MSG": "Bạn đã sử dụng Super Productivity một thời gian. Hãy thiết lập đồng bộ hóa để sao lưu dữ liệu của bạn và giữ an toàn trên mọi thiết bị.",
|
||||
"SETUP": "Thiết lập đồng bộ hóa"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_PROJECT_SETTINGS": "Thay đổi cài đặt dự án",
|
||||
"CHANGE_TAG_SETTINGS": "Thay đổi cài đặt nhãn"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "您想將 Super Productivity 安裝為 PWA 嗎?"
|
||||
},
|
||||
"B_OFFLINE": "您已斷開與網際網路的連接。同步和資料請求將失效。",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "暫不",
|
||||
"MSG": "您已經使用 Super Productivity 一段時間了。設定同步以備份您的資料,並在所有裝置上保持安全。",
|
||||
"SETUP": "設定同步"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "更改背景",
|
||||
"CHANGE_PROJECT_SETTINGS": "變更專案設定",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"MSG": "您想要将超级生产力安装为 PWA 吗?"
|
||||
},
|
||||
"B_OFFLINE": "您已断开互联网连接。同步和请求问题提供者数据将无法工作。",
|
||||
"B_SYNC_SAFETY": {
|
||||
"DISMISS": "暂不",
|
||||
"MSG": "您已经使用 Super Productivity 一段时间了。设置同步以备份您的数据,并在所有设备上保持安全。",
|
||||
"SETUP": "设置同步"
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "更改背景",
|
||||
"CHANGE_PROJECT_SETTINGS": "更改项目设置",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue