fix: account deletion times out #291

This commit is contained in:
rustmailer
2026-06-07 15:34:38 +08:00
parent 62cb5264fd
commit c736afffb0
29 changed files with 106 additions and 14 deletions
+1
View File
@@ -262,6 +262,7 @@ impl From<AccountV3> for AccountModel {
imap_quota_bytes: None, imap_quota_bytes: None,
auto_download_new_mailboxes: None, auto_download_new_mailboxes: None,
download_schedule: None, download_schedule: None,
deleting: false,
} }
} }
} }
+38 -3
View File
@@ -97,6 +97,8 @@ pub struct Account {
pub imap_quota_window: Option<QuotaWindow>, pub imap_quota_window: Option<QuotaWindow>,
pub auto_download_new_mailboxes: Option<bool>, pub auto_download_new_mailboxes: Option<bool>,
pub download_schedule: Option<String>, pub download_schedule: Option<String>,
#[serde(default)]
pub deleting: bool,
} }
impl MemDbModel for Account { impl MemDbModel for Account {
@@ -136,6 +138,7 @@ impl Account {
imap_quota_bytes: request.imap_quota_bytes, imap_quota_bytes: request.imap_quota_bytes,
imap_quota_window: request.imap_quota_window, imap_quota_window: request.imap_quota_window,
download_schedule: request.download_schedule, download_schedule: request.download_schedule,
deleting: false,
}) })
} }
@@ -223,14 +226,46 @@ impl Account {
pub async fn delete(account_id: u64) -> BichonResult<()> { pub async fn delete(account_id: u64) -> BichonResult<()> {
let account = Self::get(account_id)?; let account = Self::get(account_id)?;
// Immediately stop scheduling to prevent new downloads
if matches!(account.account_type, AccountType::IMAP) {
SYNC_TASKS.stop(account.id).await?;
}
// Mark as deleting and disabled so frontend shows status and download tasks skip it
update_impl(
DB_MANAGER.db(),
&account_id.to_string(),
move |current: Account| {
let mut updated = current.clone();
updated.deleting = true;
updated.enabled = false;
Ok(updated)
},
)?;
// Spawn background cleanup — heavy work (Tantivy, attachments) runs off the request path
tokio::spawn(async move {
if let Err(error) = Self::cleanup_account_resources_sequential(&account).await { if let Err(error) = Self::cleanup_account_resources_sequential(&account).await {
tracing::error!( tracing::error!(
"[CLEANUP_ACCOUNT_ERROR] Account {}: failed to cleanup resources: {:#?}", "[CLEANUP_ACCOUNT_ERROR] Account {}: cleanup failed, reverting deleting flag: {:#?}",
account_id, account_id,
error error
); );
return Err(error); // Revert deleting flag so the user can retry (only if account record still exists)
let _ = update_impl(
DB_MANAGER.db(),
&account_id.to_string(),
move |current: Account| {
let mut updated = current.clone();
updated.deleting = false;
updated.enabled = true;
Ok(updated)
},
);
} }
});
Ok(()) Ok(())
} }
@@ -239,8 +274,8 @@ impl Account {
} }
async fn cleanup_account_resources_sequential(account: &AccountModel) -> BichonResult<()> { async fn cleanup_account_resources_sequential(account: &AccountModel) -> BichonResult<()> {
// Sync task already stopped in delete() before spawning this background task
if matches!(account.account_type, AccountType::IMAP) { if matches!(account.account_type, AccountType::IMAP) {
SYNC_TASKS.stop(account.id).await?;
DownloadState::delete(account.id)?; DownloadState::delete(account.id)?;
} }
OAuth2AccessToken::try_delete(account.id)?; OAuth2AccessToken::try_delete(account.id)?;
+2
View File
@@ -58,6 +58,7 @@ pub struct AccountResp {
pub imap_quota_window: Option<QuotaWindow>, pub imap_quota_window: Option<QuotaWindow>,
pub auto_download_new_mailboxes: Option<bool>, pub auto_download_new_mailboxes: Option<bool>,
pub download_schedule: Option<String>, pub download_schedule: Option<String>,
pub deleting: bool,
} }
impl AccountResp { impl AccountResp {
@@ -95,6 +96,7 @@ impl AccountResp {
imap_quota_window: account.imap_quota_window, imap_quota_window: account.imap_quota_window,
auto_download_new_mailboxes: account.auto_download_new_mailboxes, auto_download_new_mailboxes: account.auto_download_new_mailboxes,
download_schedule: account.download_schedule, download_schedule: account.download_schedule,
deleting: account.deleting,
} }
} }
} }
+7
View File
@@ -113,6 +113,9 @@ impl AccountDownTask {
let account = AccountModel::get(account_id).ok(); let account = AccountModel::get(account_id).ok();
match account { match account {
Some(account) => { Some(account) => {
if account.deleting {
return Ok(());
}
if !account.enabled { if !account.enabled {
let last = LAST_WARN_TIME.load(Ordering::Relaxed); let last = LAST_WARN_TIME.load(Ordering::Relaxed);
let now = utc_now!(); let now = utc_now!();
@@ -246,6 +249,10 @@ impl AccountDownTask {
} }
}; };
if account.deleting {
return;
}
if let Err(e) = process_imap_download(&account, token_clone, TriggerType::Manual).await if let Err(e) = process_imap_download(&account, token_clone, TriggerType::Manual).await
{ {
error!("Manual download failed for {}: {:?}", account_id, e); error!("Manual download failed for {}: {:?}", account_id, e);
+1
View File
@@ -144,6 +144,7 @@ export interface AccountModel {
imap_quota_bytes?: number; imap_quota_bytes?: number;
auto_download_new_mailboxes?: boolean; auto_download_new_mailboxes?: boolean;
download_schedule?: string; download_schedule?: string;
deleting?: boolean;
} }
export const download_state = async (account_id: number) => { export const download_state = async (account_id: number) => {
@@ -50,13 +50,16 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
const hasPermission = require_any_permission(['system:root', 'account:manage'], row.original.id); const hasPermission = require_any_permission(['system:root', 'account:manage'], row.original.id);
const hasReadPermission = require_any_permission(['system:root', 'account:read_details'], row.original.id); const hasReadPermission = require_any_permission(['system:root', 'account:read_details'], row.original.id);
const isDeleting = row.original.deleting === true;
const canShowAnyAction = const canShowAnyAction =
!isDeleting && (
(hasPermission) || (hasPermission) ||
(account_type === 'IMAP' && hasPermission) || (account_type === 'IMAP' && hasPermission) ||
(account_type === 'IMAP' && hasReadPermission); (account_type === 'IMAP' && hasReadPermission)
);
const showDownload = account_type === 'IMAP' && hasPermission; const showDownload = !isDeleting && account_type === 'IMAP' && hasPermission;
const handleStartDownload = async () => { const handleStartDownload = async () => {
try { try {
@@ -43,8 +43,8 @@ export function AccountDeleteDialog({ open, onOpenChange, currentRow }: Props) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
function handleSuccess() { function handleSuccess() {
toast({ toast({
title: t('dialogs.accountDeleted'), title: t('dialogs.accountDeletionStarted'),
description: t('dialogs.accountDeletedDesc'), description: t('dialogs.accountDeletionStartedDesc'),
action: <ToastAction altText={t('common.close')}>{t('common.close')}</ToastAction>, action: <ToastAction altText={t('common.close')}>{t('common.close')}</ToastAction>,
}); });
@@ -77,7 +77,7 @@ export function EnableAction({ row }: DataTableRowActionsProps) {
<Switch <Switch
checked={row.original.enabled} checked={row.original.enabled}
onCheckedChange={() => setOpen(true)} onCheckedChange={() => setOpen(true)}
disabled={!hasPermission || updateMutation.isPending} disabled={!hasPermission || updateMutation.isPending || row.original.deleting}
/> />
<ConfirmDialog <ConfirmDialog
open={open} open={open}
@@ -35,6 +35,9 @@ export function RunningStateCellAction({ row }: Props) {
const { setOpen, setCurrentRow } = useAccountContext() const { setOpen, setCurrentRow } = useAccountContext()
const { require_any_permission } = useCurrentUser() const { require_any_permission } = useCurrentUser()
if (row.original.deleting) {
return <span className="text-xs text-muted-foreground italic">Deleting...</span>
}
let account_type = row.original.account_type; let account_type = row.original.account_type;
if (account_type === "NoSync") { if (account_type === "NoSync") {
return <span className="text-xs text-muted-foreground">n/a</span> return <span className="text-xs text-muted-foreground">n/a</span>
@@ -127,7 +127,7 @@ export function AccountTable({ columns, data }: DataTableProps) {
<TableRow <TableRow
key={row.id} key={row.id}
data-state={row.getIsSelected() && 'selected'} data-state={row.getIsSelected() && 'selected'}
className='group/row' className={row.original.deleting ? 'opacity-50' : 'group/row'}
> >
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell <TableCell
+4
View File
@@ -55,6 +55,10 @@ export default function Accounts() {
const { data: accountList, isLoading } = useQuery({ const { data: accountList, isLoading } = useQuery({
queryKey: ['account-list'], queryKey: ['account-list'],
queryFn: list_accounts, queryFn: list_accounts,
refetchInterval: (query) => {
const items = (query.state.data as { items?: { deleting?: boolean }[] })?.items;
return items?.some((item) => item.deleting) ? 5000 : false;
},
}) })
const hasAccounts = accountList != null && accountList.items.length > 0; const hasAccounts = accountList != null && accountList.items.length > 0;
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "فشل حذف الحساب", "accountDeleteFailed": "فشل حذف الحساب",
"accountDeleted": "تم حذف الحساب", "accountDeleted": "تم حذف الحساب",
"accountDeletedDesc": "تم حذف حسابك بنجاح.", "accountDeletedDesc": "تم حذف حسابك بنجاح.",
"accountDeletionStarted": "بدء حذف الحساب",
"accountDeletionStartedDesc": "جاري حذف الحساب في الخلفية، وسيختفي بعد اكتمال التنظيف.",
"allResourcesErased": "سيتم مسح جميع الموارد ذات الصلة نهائيًا.", "allResourcesErased": "سيتم مسح جميع الموارد ذات الصلة نهائيًا.",
"cannotBeUndone": "لا يمكن التراجع عن هذا الإجراء!", "cannotBeUndone": "لا يمكن التراجع عن هذا الإجراء!",
"confirmDelete": "تأكيد الحذف", "confirmDelete": "تأكيد الحذف",
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "Sletning af konto mislykkedes", "accountDeleteFailed": "Sletning af konto mislykkedes",
"accountDeleted": "Konto slettet", "accountDeleted": "Konto slettet",
"accountDeletedDesc": "Din konto er blevet slettet.", "accountDeletedDesc": "Din konto er blevet slettet.",
"accountDeletionStarted": "Kontoen slettes nu",
"accountDeletionStartedDesc": "Kontoen slettes i baggrunden og forsvinder, når oprydningen er færdig.",
"allResourcesErased": "Alle relaterede ressourcer vil blive slettet permanent.", "allResourcesErased": "Alle relaterede ressourcer vil blive slettet permanent.",
"cannotBeUndone": "Denne handling kan ikke fortrydes!", "cannotBeUndone": "Denne handling kan ikke fortrydes!",
"confirmDelete": "Bekræft Sletning", "confirmDelete": "Bekræft Sletning",
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "Löschen des Kontos fehlgeschlagen", "accountDeleteFailed": "Löschen des Kontos fehlgeschlagen",
"accountDeleted": "Konto gelöscht", "accountDeleted": "Konto gelöscht",
"accountDeletedDesc": "Ihr Konto wurde erfolgreich gelöscht.", "accountDeletedDesc": "Ihr Konto wurde erfolgreich gelöscht.",
"accountDeletionStarted": "Kontolöschung gestartet",
"accountDeletionStartedDesc": "Konto wird im Hintergrund gelöscht und verschwindet nach der Bereinigung.",
"allResourcesErased": "Alle zugehörigen Ressourcen werden dauerhaft gelöscht.", "allResourcesErased": "Alle zugehörigen Ressourcen werden dauerhaft gelöscht.",
"cannotBeUndone": "Diese Aktion kann nicht rückgängig gemacht werden!", "cannotBeUndone": "Diese Aktion kann nicht rückgängig gemacht werden!",
"confirmDelete": "Löschung bestätigen", "confirmDelete": "Löschung bestätigen",
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "Account delete Failed", "accountDeleteFailed": "Account delete Failed",
"accountDeleted": "Account Deleted", "accountDeleted": "Account Deleted",
"accountDeletedDesc": "Your account has been successfully deleted.", "accountDeletedDesc": "Your account has been successfully deleted.",
"accountDeletionStarted": "Account deletion started",
"accountDeletionStartedDesc": "Account is being deleted in the background and will disappear after cleanup.",
"allResourcesErased": "All related resources will be permanently erased.", "allResourcesErased": "All related resources will be permanently erased.",
"cannotBeUndone": "This action cannot be undone!", "cannotBeUndone": "This action cannot be undone!",
"confirmDelete": "Confirm Delete", "confirmDelete": "Confirm Delete",
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "Error al eliminar la cuenta", "accountDeleteFailed": "Error al eliminar la cuenta",
"accountDeleted": "Cuenta eliminada", "accountDeleted": "Cuenta eliminada",
"accountDeletedDesc": "Tu cuenta ha sido eliminada con éxito.", "accountDeletedDesc": "Tu cuenta ha sido eliminada con éxito.",
"accountDeletionStarted": "Eliminación de cuenta iniciada",
"accountDeletionStartedDesc": "La cuenta se está eliminando en segundo plano y desaparecerá tras la limpieza.",
"allResourcesErased": "Todos los recursos asociados se borrarán permanentemente.", "allResourcesErased": "Todos los recursos asociados se borrarán permanentemente.",
"cannotBeUndone": "¡Esta acción no se puede deshacer!", "cannotBeUndone": "¡Esta acción no se puede deshacer!",
"confirmDelete": "Confirmar eliminación", "confirmDelete": "Confirmar eliminación",
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "Tilin poistaminen epäonnistui", "accountDeleteFailed": "Tilin poistaminen epäonnistui",
"accountDeleted": "Tili poistettu", "accountDeleted": "Tili poistettu",
"accountDeletedDesc": "Tilisi on poistettu onnistuneesti.", "accountDeletedDesc": "Tilisi on poistettu onnistuneesti.",
"accountDeletionStarted": "Tilin poistaminen aloitettu",
"accountDeletionStartedDesc": "Tiliä poistetaan taustalla. Se katoaa, kun puhdistus on valmis.",
"allResourcesErased": "Kaikki liittyvät resurssit poistetaan pysyvästi.", "allResourcesErased": "Kaikki liittyvät resurssit poistetaan pysyvästi.",
"cannotBeUndone": "Tätä toimenpidettä ei voi kumota!", "cannotBeUndone": "Tätä toimenpidettä ei voi kumota!",
"confirmDelete": "Vahvista poisto", "confirmDelete": "Vahvista poisto",
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "Échec de la suppression du compte", "accountDeleteFailed": "Échec de la suppression du compte",
"accountDeleted": "Compte Supprimé", "accountDeleted": "Compte Supprimé",
"accountDeletedDesc": "Votre compte a été supprimé avec succès.", "accountDeletedDesc": "Votre compte a été supprimé avec succès.",
"accountDeletionStarted": "Suppression du compte lancée",
"accountDeletionStartedDesc": "Compte en cours de suppression en arrière-plan, disparaîtra après nettoyage.",
"allResourcesErased": "Toutes les ressources associées seront effacées définitivement.", "allResourcesErased": "Toutes les ressources associées seront effacées définitivement.",
"cannotBeUndone": "Cette action ne peut pas être annulée !", "cannotBeUndone": "Cette action ne peut pas être annulée !",
"confirmDelete": "Confirmer la Suppression", "confirmDelete": "Confirmer la Suppression",
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "Eliminazione account fallita", "accountDeleteFailed": "Eliminazione account fallita",
"accountDeleted": "Account Eliminato", "accountDeleted": "Account Eliminato",
"accountDeletedDesc": "Il tuo account è stato eliminato con successo.", "accountDeletedDesc": "Il tuo account è stato eliminato con successo.",
"accountDeletionStarted": "Eliminazione account avviata",
"accountDeletionStartedDesc": "L'account è in fase di eliminazione in background e scomparirà dopo la pulizia.",
"allResourcesErased": "Tutte le risorse correlate verranno cancellate permanentemente.", "allResourcesErased": "Tutte le risorse correlate verranno cancellate permanentemente.",
"cannotBeUndone": "Questa azione non può essere annullata!", "cannotBeUndone": "Questa azione non può essere annullata!",
"confirmDelete": "Conferma Eliminazione", "confirmDelete": "Conferma Eliminazione",
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "アカウントの削除に失敗しました", "accountDeleteFailed": "アカウントの削除に失敗しました",
"accountDeleted": "アカウントが削除されました", "accountDeleted": "アカウントが削除されました",
"accountDeletedDesc": "アカウントが正常に削除されました。", "accountDeletedDesc": "アカウントが正常に削除されました。",
"accountDeletionStarted": "アカウントの削除を開始しました",
"accountDeletionStartedDesc": "バックグラウンドで削除中です。完了するとリストから消えます。",
"allResourcesErased": "関連するすべてのリソースは完全に消去されます。", "allResourcesErased": "関連するすべてのリソースは完全に消去されます。",
"cannotBeUndone": "この操作は元に戻せません!", "cannotBeUndone": "この操作は元に戻せません!",
"confirmDelete": "削除の確認", "confirmDelete": "削除の確認",
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "계정 삭제 실패", "accountDeleteFailed": "계정 삭제 실패",
"accountDeleted": "계정 삭제됨", "accountDeleted": "계정 삭제됨",
"accountDeletedDesc": "계정이 성공적으로 삭제되었습니다.", "accountDeletedDesc": "계정이 성공적으로 삭제되었습니다.",
"accountDeletionStarted": "계정 삭제 시작됨",
"accountDeletionStartedDesc": "백그라운드에서 삭제 중이며, 정리가 끝나면 목록에서 사라집니다.",
"allResourcesErased": "모든 관련 리소스가 영구적으로 지워집니다.", "allResourcesErased": "모든 관련 리소스가 영구적으로 지워집니다.",
"cannotBeUndone": "이 작업은 되돌릴 수 없습니다!", "cannotBeUndone": "이 작업은 되돌릴 수 없습니다!",
"confirmDelete": "삭제 확인", "confirmDelete": "삭제 확인",
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "Account verwijderen Mislukt", "accountDeleteFailed": "Account verwijderen Mislukt",
"accountDeleted": "Account Verwijderd", "accountDeleted": "Account Verwijderd",
"accountDeletedDesc": "Uw account is succesvol verwijderd.", "accountDeletedDesc": "Uw account is succesvol verwijderd.",
"accountDeletionStarted": "Verwijdering account gestart",
"accountDeletionStartedDesc": "Account wordt op de achtergrond verwijderd en verdwijnt na opschonen.",
"allResourcesErased": "Alle gerelateerde bronnen worden permanent gewist.", "allResourcesErased": "Alle gerelateerde bronnen worden permanent gewist.",
"cannotBeUndone": "Deze actie kan niet ongedaan worden gemaakt!", "cannotBeUndone": "Deze actie kan niet ongedaan worden gemaakt!",
"confirmDelete": "Verwijdering Bevestigen", "confirmDelete": "Verwijdering Bevestigen",
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "Sletting av konto mislyktes", "accountDeleteFailed": "Sletting av konto mislyktes",
"accountDeleted": "Konto slettet", "accountDeleted": "Konto slettet",
"accountDeletedDesc": "Kontoen din har blitt slettet.", "accountDeletedDesc": "Kontoen din har blitt slettet.",
"accountDeletionStarted": "Kontosletting startet",
"accountDeletionStartedDesc": "Kontoen slettes i bakgrunnen og forsvinner når opprydningen er ferdig.",
"allResourcesErased": "Alle relaterte ressurser vil bli permanent slettet.", "allResourcesErased": "Alle relaterte ressurser vil bli permanent slettet.",
"cannotBeUndone": "Denne handlingen kan ikke angres!", "cannotBeUndone": "Denne handlingen kan ikke angres!",
"confirmDelete": "Bekreft sletting", "confirmDelete": "Bekreft sletting",
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "Bład podczas usuwania konta", "accountDeleteFailed": "Bład podczas usuwania konta",
"accountDeleted": "Konto usunięte", "accountDeleted": "Konto usunięte",
"accountDeletedDesc": "Konto zostało usunięte.", "accountDeletedDesc": "Konto zostało usunięte.",
"accountDeletionStarted": "Rozpoczęto usuwanie konta",
"accountDeletionStartedDesc": "Konto jest usuwane w tle i zniknie po zakończeniu czyszczenia.",
"allResourcesErased": "Wszystkie powiązane zasoby zostaną trwale usunięte.", "allResourcesErased": "Wszystkie powiązane zasoby zostaną trwale usunięte.",
"cannotBeUndone": "Tej czynności nie można cofnąć!", "cannotBeUndone": "Tej czynności nie można cofnąć!",
"confirmDelete": "Potwierdź usunięcie", "confirmDelete": "Potwierdź usunięcie",
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "Falha ao Excluir Conta", "accountDeleteFailed": "Falha ao Excluir Conta",
"accountDeleted": "Conta Excluída", "accountDeleted": "Conta Excluída",
"accountDeletedDesc": "A conta foi excluída com sucesso.", "accountDeletedDesc": "A conta foi excluída com sucesso.",
"accountDeletionStarted": "Exclusão da conta iniciada",
"accountDeletionStartedDesc": "A conta está sendo excluída em segundo plano e desaparecerá após a limpeza.",
"allResourcesErased": "Todos os recursos relacionados serão permanentemente apagados.", "allResourcesErased": "Todos os recursos relacionados serão permanentemente apagados.",
"cannotBeUndone": "Esta ação não pode ser desfeita!", "cannotBeUndone": "Esta ação não pode ser desfeita!",
"confirmDelete": "Confirmar Exclusão", "confirmDelete": "Confirmar Exclusão",
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "Ошибка удаления аккаунта", "accountDeleteFailed": "Ошибка удаления аккаунта",
"accountDeleted": "Аккаунт удален", "accountDeleted": "Аккаунт удален",
"accountDeletedDesc": "Ваш аккаунт был успешно удален.", "accountDeletedDesc": "Ваш аккаунт был успешно удален.",
"accountDeletionStarted": "Удаление аккаунта запущено",
"accountDeletionStartedDesc": "Аккаунт удаляется в фоновом режиме и исчезнет после очистки.",
"allResourcesErased": "Все связанные ресурсы будут безвозвратно стерты.", "allResourcesErased": "Все связанные ресурсы будут безвозвратно стерты.",
"cannotBeUndone": "Это действие нельзя отменить!", "cannotBeUndone": "Это действие нельзя отменить!",
"confirmDelete": "Подтвердить удаление", "confirmDelete": "Подтвердить удаление",
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "Borttagning av konto misslyckades", "accountDeleteFailed": "Borttagning av konto misslyckades",
"accountDeleted": "Konto raderat", "accountDeleted": "Konto raderat",
"accountDeletedDesc": "Ditt konto har tagits bort.", "accountDeletedDesc": "Ditt konto har tagits bort.",
"accountDeletionStarted": "Kontoradering har startat",
"accountDeletionStartedDesc": "Kontot raderas i bakgrunden och försvinner när rensningen är klar.",
"allResourcesErased": "Alla relaterade resurser kommer att raderas permanent.", "allResourcesErased": "Alla relaterade resurser kommer att raderas permanent.",
"cannotBeUndone": "Denna åtgärd kan inte ångras!", "cannotBeUndone": "Denna åtgärd kan inte ångras!",
"confirmDelete": "Bekräfta borttagning", "confirmDelete": "Bekräfta borttagning",
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "帳號刪除失敗", "accountDeleteFailed": "帳號刪除失敗",
"accountDeleted": "帳號已刪除", "accountDeleted": "帳號已刪除",
"accountDeletedDesc": "帳號已成功刪除。", "accountDeletedDesc": "帳號已成功刪除。",
"accountDeletionStarted": "帳戶刪除已開始",
"accountDeletionStartedDesc": "帳戶正在背景刪除,清理完成後將從列表中消失。",
"allResourcesErased": "所有相關資源將被永久清除。", "allResourcesErased": "所有相關資源將被永久清除。",
"cannotBeUndone": "此操作無法復原!", "cannotBeUndone": "此操作無法復原!",
"confirmDelete": "確認刪除", "confirmDelete": "確認刪除",
+2
View File
@@ -536,6 +536,8 @@
"accountDeleteFailed": "账户删除失败", "accountDeleteFailed": "账户删除失败",
"accountDeleted": "账户已删除", "accountDeleted": "账户已删除",
"accountDeletedDesc": "您的账户已成功删除。", "accountDeletedDesc": "您的账户已成功删除。",
"accountDeletionStarted": "账户删除已开始",
"accountDeletionStartedDesc": "账户正在后台删除,清理完成后将从列表中消失。",
"allResourcesErased": "所有相关资源将被永久删除。", "allResourcesErased": "所有相关资源将被永久删除。",
"cannotBeUndone": "此操作无法撤销!", "cannotBeUndone": "此操作无法撤销!",
"confirmDelete": "确认删除", "confirmDelete": "确认删除",