diff --git a/app/(main)/[locale]/layout.tsx b/app/(main)/[locale]/layout.tsx index b4e444621..2aebb8614 100644 --- a/app/(main)/[locale]/layout.tsx +++ b/app/(main)/[locale]/layout.tsx @@ -17,6 +17,8 @@ import { PluginAppEvents } from "@/components/providers/plugin-app-events"; import { PluginOAuthCallbackListener } from "@/components/providers/plugin-oauth-callback-listener"; import { PWAInstallPrompt } from "@/components/pwa-install-prompt"; import { PushNotificationPrompt } from "@/components/push-notification-prompt"; +import { AccountArchiveRestorePrompt } from "@/components/account-archive-prompt"; +import { IS_LITE } from "@/lib/lite"; import { setRequestLocale } from "next-intl/server"; import { locales } from "@/i18n/routing"; import { generateLiteLocaleParams } from "@/lib/lite-static-params"; @@ -67,6 +69,8 @@ export default async function LocaleLayout({ + {/* Lite is a static bundle: no server keeps an archive there. */} + {!IS_LITE && } diff --git a/components/__tests__/account-archive-prompt.test.tsx b/components/__tests__/account-archive-prompt.test.tsx new file mode 100644 index 000000000..96f9e77b3 --- /dev/null +++ b/components/__tests__/account-archive-prompt.test.tsx @@ -0,0 +1,92 @@ +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { AccountArchiveRestorePrompt } from '../account-archive-prompt'; +import { encryptArchive } from '@/lib/account-archive'; +import { useAccountStore } from '@/stores/account-store'; +import { useAuthStore } from '@/stores/auth-store'; +import { usePolicyStore } from '@/stores/policy-store'; + +const mocks = vi.hoisted(() => ({ apiFetch: vi.fn(), stored: null as unknown })); + +vi.mock('next-intl', () => { + const t = (key: string, values?: Record) => values ? `${key} ${JSON.stringify(values)}` : key; + return { useTranslations: () => t, useLocale: () => 'en' }; +}); +vi.mock('@/lib/browser-navigation', async (importOriginal) => ({ + ...(await importOriginal()), + apiFetch: mocks.apiFetch, +})); + +const owner = { username: 'owner@x.test', serverUrl: 'https://mail.x.test' }; +const account = { + id: 'owner@x.test@mail.x.test', label: 'Work', ...owner, authMode: 'basic' as const, rememberMe: true, cookieSlot: 0, + displayName: 'Work', email: owner.username, isConnected: true, hasError: false, lastLoginAt: 1, avatarColor: '#2563eb', isDefault: true, +}; +const other = { username: 'other@y.test', serverUrl: 'https://mail.y.test', label: 'Home', avatarColor: '#16a34a' }; +const PASSWORD = 'a long backup password'; + +beforeEach(async () => { + localStorage.clear(); + mocks.stored = await encryptArchive(owner, [{ ...owner, label: 'Work', avatarColor: '#2563eb' }, other], PASSWORD); + // Only the archive answers; anything else the app asks for (the policy, say) is not there. + mocks.apiFetch.mockReset().mockImplementation(async (url: string) => url === '/api/account-archive' + ? new Response(JSON.stringify({ envelope: mocks.stored }), { status: 200 }) + : new Response('{}', { status: 404 })); + usePolicyStore.setState(s => ({ policy: { ...s.policy, features: { ...s.policy.features, accountArchiveEnabled: true } } })); + useAuthStore.setState({ ...owner, isAuthenticated: true, isDemoMode: false }); + useAccountStore.setState({ accounts: [account] as never, activeAccountId: account.id, defaultAccountId: account.id }); +}); + +const archiveCalls = () => mocks.apiFetch.mock.calls.filter(([url]) => url === '/api/account-archive').length; +const fill = (value: string) => fireEvent.change(screen.getByLabelText('password'), { target: { value } }); + +describe('AccountArchiveRestorePrompt', () => { + it('offers the saved list on a browser with only the signed-in account, and restores it', { timeout: 20_000 }, async () => { + render(); + expect(await screen.findByRole('dialog')).toHaveTextContent('prompt_title'); + fill('not the password'); + fireEvent.submit(screen.getByLabelText('password').closest('form')!); + expect(await screen.findByRole('alert', {}, { timeout: 5000 })).toHaveTextContent('errors.unlock_failed'); + + fill(PASSWORD); + fireEvent.submit(screen.getByLabelText('password').closest('form')!); + await waitFor(() => expect(screen.getByRole('status')).toHaveTextContent('restored'), { timeout: 5000 }); + expect(useAccountStore.getState().accounts.map(a => a.username)).toEqual([owner.username, other.username]); + + fireEvent.click(screen.getByRole('button', { name: 'prompt_done' })); + expect(screen.queryByRole('dialog')).toBeNull(); + }); + + it('does not ask again in this browser once declined', async () => { + const { unmount } = render(); + fireEvent.click(await screen.findByRole('button', { name: 'prompt_not_now' })); + expect(screen.queryByRole('dialog')).toBeNull(); + unmount(); + + render(); + await act(async () => { await Promise.resolve(); }); + expect(screen.queryByRole('dialog')).toBeNull(); + expect(archiveCalls()).toBe(1); + }); + + it('stays quiet when this browser already holds other accounts', async () => { + useAccountStore.setState({ accounts: [account, { ...account, id: 'b', username: 'b@x.test' }] as never }); + render(); + await act(async () => { await Promise.resolve(); }); + expect(archiveCalls()).toBe(0); + }); + + it('stays quiet when nothing is saved or the feature is off', async () => { + mocks.stored = null; + const { unmount } = render(); + await waitFor(() => expect(archiveCalls()).toBe(1)); + expect(screen.queryByRole('dialog')).toBeNull(); + unmount(); + + mocks.apiFetch.mockClear(); + usePolicyStore.setState(s => ({ policy: { ...s.policy, features: { ...s.policy.features, accountArchiveEnabled: false } } })); + render(); + await act(async () => { await Promise.resolve(); }); + expect(archiveCalls()).toBe(0); + }); +}); diff --git a/components/account-archive-prompt.tsx b/components/account-archive-prompt.tsx new file mode 100644 index 000000000..d92918167 --- /dev/null +++ b/components/account-archive-prompt.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { useCallback, useEffect, useId, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { useTranslations } from 'next-intl'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { useFocusTrap } from '@/hooks/use-focus-trap'; +import { useConfig } from '@/hooks/use-config'; +import { useAccountStore } from '@/stores/account-store'; +import { useAuthStore } from '@/stores/auth-store'; +import { usePolicyStore } from '@/stores/policy-store'; +import { archiveIdentity, decryptArchive, type ArchiveOwner } from '@/lib/account-archive'; +import { fetchArchive, restoreArchive } from '@/lib/account-archive-client'; +import { toUnicodeEmail } from '@/lib/idn'; +import { archiveErrorText, restoreSummary } from '@/components/settings/account-archive-settings'; + +const promptKey = (owner: ArchiveOwner) => `account-archive-prompt:${archiveIdentity(owner)}`; + +function wasOffered(owner: ArchiveOwner): boolean { + try { return localStorage.getItem(promptKey(owner)) !== null; } catch { return false; } +} +function markOffered(owner: ArchiveOwner) { + try { localStorage.setItem(promptKey(owner), 'offered'); } catch { /* storage unavailable */ } +} + +/** + * On a browser that knows only the account just signed in to, offers once to + * bring back the account list saved for it. Declining leaves the restore in + * Settings; the offer does not come back for that account in this browser. + */ +export function AccountArchiveRestorePrompt() { + const enabled = usePolicyStore(s => s.isFeatureEnabled('accountArchiveEnabled')); + const signedIn = useAuthStore(s => s.isAuthenticated && !s.isDemoMode); + const username = useAuthStore(s => s.username); + const serverUrl = useAuthStore(s => s.serverUrl); + // A browser that already holds other accounts is not a new device. + const onlyAccount = useAccountStore(s => s.accounts.length === 1); + const [owner, setOwner] = useState(null); + + useEffect(() => { + // An open offer stays open while the restore adds accounts; signing in to + // another account instead hides it (the check before rendering). + if (!enabled || !signedIn || !onlyAccount || !username || !serverUrl) return; + const candidate = { username, serverUrl }; + if (wasOffered(candidate)) return; + let cancelled = false; + fetchArchive(candidate) + .then(envelope => { if (!cancelled && envelope) setOwner(candidate); }) + .catch(() => { /* An optional lookup must not get in the way of mail. */ }); + return () => { cancelled = true; }; + }, [enabled, signedIn, onlyAccount, username, serverUrl]); + + const close = useCallback(() => { + if (owner) markOffered(owner); + setOwner(null); + }, [owner]); + + if (!owner || owner.username !== username || owner.serverUrl !== serverUrl) return null; + return createPortal(, document.body); +} + +function RestoreDialog({ owner, close }: { owner: ArchiveOwner; close: () => void }) { + const t = useTranslations('settings.account.backup'); + const id = useId(); + const { rememberMeEnabled } = useConfig(); + const [password, setPassword] = useState(''); + const [busy, setBusy] = useState(false); + const [message, setMessage] = useState<{ error: boolean; text: string } | null>(null); + const done = message !== null && !message.error; + const onEscape = useCallback(() => { if (!busy) close(); }, [busy, close]); + const ref = useFocusTrap({ isActive: true, onEscape, restoreFocus: true }); + + const restore = async () => { + setBusy(true); setMessage(null); + try { + const envelope = await fetchArchive(owner); + if (!envelope) throw new Error('storage_failed'); + const result = await restoreArchive(await decryptArchive(owner, envelope, password), rememberMeEnabled); + setPassword(''); + setMessage({ error: false, text: restoreSummary(t, result) }); + } catch (err) { + setMessage({ error: true, text: archiveErrorText(t, err) }); + } finally { setBusy(false); } + }; + + return ( +
+
+

{t('prompt_title')}

+

+ {t('prompt_description', { account: toUnicodeEmail(owner.username) })} +

+ {done ? ( + <> +

{message?.text}

+
+ +
+ + ) : ( +
{ e.preventDefault(); void restore(); }}> + + setPassword(e.target.value)} + /> +

{t('restore_hint')}

+ {message &&

{message.text}

} +
+ + +
+
+ )} +
+
+ ); +} diff --git a/components/settings/account-archive-settings.tsx b/components/settings/account-archive-settings.tsx index 15195db31..c1c33b660 100644 --- a/components/settings/account-archive-settings.tsx +++ b/components/settings/account-archive-settings.tsx @@ -14,6 +14,24 @@ import { SettingsSection } from './settings-section'; const KNOWN_ERRORS = ['mismatch', 'password_length', 'unlock_failed', 'owner_signin_required', 'https_required']; +type BackupT = ReturnType; + +/** The text for a failed save, restore or delete. */ +export function archiveErrorText(t: BackupT, err: unknown): string { + const code = err instanceof Error ? err.message : ''; + return t(`errors.${KNOWN_ERRORS.includes(code) ? code : 'storage_failed'}`); +} + +/** What a restore did, in one status line. */ +export function restoreSummary(t: BackupT, result: Awaited>): string { + return [ + t(result.waiting ? 'restored' : 'restored_signed_in', result), + result.signedIn ? t('signed_in', { count: result.signedIn }) : '', + result.signInFailed ? t('sign_in_failed', { count: result.signInFailed }) : '', + result.skipped ? t('skipped', { count: result.skipped }) : '', + ].join(' ').trim(); +} + /** Save this browser's account list, encrypted, under the active account; restore it on another device. */ export function AccountArchiveSettings() { const t = useTranslations('settings.account.backup'); @@ -53,10 +71,7 @@ export function AccountArchiveSettings() { open(null); setMessage({ error: false, text }); } - catch (err) { - const code = err instanceof Error ? err.message : ''; - setMessage({ error: true, text: t(`errors.${KNOWN_ERRORS.includes(code) ? code : 'storage_failed'}`) }); - } finally { setBusy(false); } + catch (err) { setMessage({ error: true, text: archiveErrorText(t, err) }); } finally { setBusy(false); } }; const save = () => run(async () => { @@ -70,13 +85,7 @@ export function AccountArchiveSettings() { const restore = () => run(async () => { const envelope = await fetchArchive(owner); if (!envelope) { setSaved(false); throw new Error('storage_failed'); } - const result = await restoreArchive(await decryptArchive(owner, envelope, password), rememberMeEnabled); - return [ - t(result.waiting ? 'restored' : 'restored_signed_in', result), - result.signedIn ? t('signed_in', { count: result.signedIn }) : '', - result.signInFailed ? t('sign_in_failed', { count: result.signInFailed }) : '', - result.skipped ? t('skipped', { count: result.skipped }) : '', - ].join(' ').trim(); + return restoreSummary(t, await restoreArchive(await decryptArchive(owner, envelope, password), rememberMeEnabled)); }); const remove = () => { if (!window.confirm(t('delete_confirm'))) return; diff --git a/locales/ar/common.json b/locales/ar/common.json index 9e71b7d94..4eba447ef 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -1777,6 +1777,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/ca/common.json b/locales/ca/common.json index ea9fe96ff..aea828c12 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -1777,6 +1777,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/cs/common.json b/locales/cs/common.json index df339741b..438bbbd77 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -1774,6 +1774,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/da/common.json b/locales/da/common.json index 00fe3b0f9..9105ecac8 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -1777,6 +1777,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/de/common.json b/locales/de/common.json index 3fe99327e..184557675 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -1774,6 +1774,10 @@ "signed_in": "Bei {count, plural, one {# Konto} other {# Konten}} mit dem gespeicherten Passwort angemeldet.", "sign_in_failed": "{count, plural, one {# gespeichertes Passwort wurde} other {# gespeicherte Passwörter wurden}} abgelehnt: Melden Sie sich bei diesen Konten über die Kontoliste an.", "deleted": "Gespeicherte Liste gelöscht.", + "prompt_title": "Weitere Konten wiederherstellen?", + "prompt_description": "Für {account} ist eine Kontenliste gespeichert. Gib das Backup-Passwort ein, um die Konten in diesem Browser hinzuzufügen, oder mach es später in den Einstellungen.", + "prompt_not_now": "Nicht jetzt", + "prompt_done": "Fertig", "errors": { "mismatch": "Die Passwörter stimmen nicht überein.", "password_length": "Verwenden Sie mindestens 10 Zeichen.", diff --git a/locales/en/common.json b/locales/en/common.json index b9d547a9c..69f8632a7 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -1777,6 +1777,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/es/common.json b/locales/es/common.json index 305b43674..93852fa54 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -1774,6 +1774,10 @@ "signed_in": "Sesión iniciada en {count, plural, one {# cuenta} other {# cuentas}} con la contraseña guardada.", "sign_in_failed": "{count, plural, one {# contraseña guardada fue rechazada} other {# contraseñas guardadas fueron rechazadas}}: inicia sesión en esas cuentas desde la lista de cuentas.", "deleted": "Lista guardada eliminada.", + "prompt_title": "¿Restaurar tus otras cuentas?", + "prompt_description": "Hay una lista de cuentas guardada para {account}. Introduce la contraseña de la copia para añadirlas a este navegador, o hazlo más tarde desde Ajustes.", + "prompt_not_now": "Ahora no", + "prompt_done": "Listo", "errors": { "mismatch": "Las contraseñas no coinciden.", "password_length": "Usa al menos 10 caracteres.", diff --git a/locales/et/common.json b/locales/et/common.json index a8cb1e362..ee08f7f40 100644 --- a/locales/et/common.json +++ b/locales/et/common.json @@ -1762,6 +1762,10 @@ "signed_in": "Salvestatud parooliga logiti sisse {count, plural, one {# kontosse} other {# kontosse}}.", "sign_in_failed": "{count, plural, one {# salvestatud parool lükati tagasi} other {# salvestatud parooli lükati tagasi}}: logi neisse kontodesse kontode nimekirjast sisse.", "deleted": "Salvestatud nimekiri kustutatud.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "Paroolid ei kattu.", "password_length": "Kasuta vähemalt 10 märki.", diff --git a/locales/fa/common.json b/locales/fa/common.json index 15f0843c5..073d78a33 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -1777,6 +1777,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/fr/common.json b/locales/fr/common.json index 0bd1ee8b6..18fa76142 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -1774,6 +1774,10 @@ "signed_in": "Connecté à {count, plural, one {# compte} other {# comptes}} avec le mot de passe enregistré.", "sign_in_failed": "{count, plural, one {# mot de passe enregistré a été refusé} other {# mots de passe enregistrés ont été refusés}} : connectez-vous à ces comptes depuis la liste des comptes.", "deleted": "Liste enregistrée supprimée.", + "prompt_title": "Restaurer vos autres comptes ?", + "prompt_description": "Une liste de comptes est enregistrée pour {account}. Saisissez le mot de passe de la sauvegarde pour les ajouter à ce navigateur, ou faites-le plus tard dans les Paramètres.", + "prompt_not_now": "Plus tard", + "prompt_done": "Terminé", "errors": { "mismatch": "Les mots de passe ne correspondent pas.", "password_length": "Utilisez au moins 10 caractères.", diff --git a/locales/he/common.json b/locales/he/common.json index f7323b901..8046eeade 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -1739,6 +1739,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/hu/common.json b/locales/hu/common.json index aa004a92e..80da31b97 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -1777,6 +1777,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/it/common.json b/locales/it/common.json index 528d5f530..38d1b9074 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -1774,6 +1774,10 @@ "signed_in": "Accesso fatto a {count, plural, one {# account} other {# account}} con la password salvata.", "sign_in_failed": "{count, plural, one {# password salvata è stata rifiutata} other {# password salvate sono state rifiutate}}: accedi a quegli account dall'elenco account.", "deleted": "Elenco salvato eliminato.", + "prompt_title": "Ripristinare gli altri account?", + "prompt_description": "Per {account} è salvato un elenco di account. Inserisci la password del backup per aggiungerli a questo browser, oppure fallo più tardi dalle Impostazioni.", + "prompt_not_now": "Non ora", + "prompt_done": "Fatto", "errors": { "mismatch": "Le password non coincidono.", "password_length": "Usa almeno 10 caratteri.", diff --git a/locales/ja/common.json b/locales/ja/common.json index 0d90ce2aa..6fc41dbb7 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -1774,6 +1774,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/ko/common.json b/locales/ko/common.json index 2f9f59c95..5f44db81f 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -1774,6 +1774,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/lv/common.json b/locales/lv/common.json index 8784a9859..82795ceef 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -1774,6 +1774,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/mn/common.json b/locales/mn/common.json index 9bd741777..31b8b9a98 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -1777,6 +1777,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/nb/common.json b/locales/nb/common.json index a5800c956..51bf271a8 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -1777,6 +1777,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/nl/common.json b/locales/nl/common.json index a48711b70..b5566c650 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -1774,6 +1774,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/pl/common.json b/locales/pl/common.json index 296afebe8..5d6512899 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -1774,6 +1774,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/pt/common.json b/locales/pt/common.json index d624dcf1f..f9220ba25 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -1774,6 +1774,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/ro/common.json b/locales/ro/common.json index e3390e866..9bef3509f 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -1777,6 +1777,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/ru/common.json b/locales/ru/common.json index 1fa117545..5755edfbb 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -1774,6 +1774,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/sk/common.json b/locales/sk/common.json index 090c74190..f46c7c87c 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -1777,6 +1777,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/tr/common.json b/locales/tr/common.json index b669457e3..31a527d69 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -1774,6 +1774,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/uk/common.json b/locales/uk/common.json index 02bc6fffe..898f6e7ba 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -1774,6 +1774,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index 865e600b2..992040bfc 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -1777,6 +1777,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.", diff --git a/locales/zh/common.json b/locales/zh/common.json index fc53f5be2..d055db660 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -1774,6 +1774,10 @@ "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", + "prompt_title": "Restore your other accounts?", + "prompt_description": "A list of accounts is saved for {account}. Enter its backup password to add them to this browser, or do it later from Settings.", + "prompt_not_now": "Not now", + "prompt_done": "Done", "errors": { "mismatch": "The passwords do not match.", "password_length": "Use at least 10 characters.",