diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 679266b82..c30bc7c55 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -74,6 +74,8 @@ }, "error": { "pdfPassword": "The PDF Document is passworded and either the password was not provided or was incorrect", + "encryptedPdfMustRemovePassword": "This PDF is encrypted or password-protected. Please unlock it before converting to PDF/A.", + "incorrectPasswordProvided": "The PDF password is incorrect or not provided.", "_value": "Error", "dismissAllErrors": "Dismiss All Errors", "sorry": "Sorry for the issue!", diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index 68cb57546..ae23ddd73 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -68,6 +68,8 @@ }, "error": { "pdfPassword": "The PDF Document is passworded and either the password was not provided or was incorrect", + "encryptedPdfMustRemovePassword": "This PDF is encrypted or password-protected. Please unlock it before converting to PDF/A.", + "incorrectPasswordProvided": "The PDF password is incorrect or not provided.", "_value": "Error", "sorry": "Sorry for the issue!", "needHelp": "Need help / Found an issue?", diff --git a/frontend/src/services/http.ts b/frontend/src/services/http.ts index c8cca7930..35a79ca1e 100644 --- a/frontend/src/services/http.ts +++ b/frontend/src/services/http.ts @@ -1,6 +1,7 @@ import axios from 'axios'; import { alert } from '../components/toast'; import { broadcastErroredFiles, extractErrorFileIds, normalizeAxiosErrorData } from './errorUtils'; +import { showSpecialErrorToast } from './specialErrorToasts'; const FRIENDLY_FALLBACK = 'There was an error processing your request.'; @@ -49,6 +50,11 @@ function extractAxiosErrorMessage(error: any): { title: string; body: string } { if (typeof raw === 'string') return raw; try { return JSON.stringify(data); } catch { return ''; } })(); + // Specific friendly mapping for encrypted PDFs (centralized toast also fires in interceptor) + if (ENCRYPTION_ERROR_REGEX.test(body)) { + const title = titleForStatus(error.response?.status); + return { title, body: ENCRYPTION_FRIENDLY }; + } const ids = extractIds(); const title = titleForStatus(status); if (ids && ids.length > 0) { @@ -64,6 +70,9 @@ function extractAxiosErrorMessage(error: any): { title: string; body: string } { } try { const msg = (error?.message || String(error)) as string; + if (ENCRYPTION_ERROR_REGEX.test(msg)) { + return { title: 'Request error', body: ENCRYPTION_FRIENDLY }; + } return { title: 'Network error', body: isUnhelpfulMessage(msg) ? FRIENDLY_FALLBACK : msg }; } catch (e) { // ignore extraction errors @@ -109,7 +118,17 @@ const __INTERCEPTOR_ID__ = axios.interceptors.response.use( } } - alert({ alertType: 'error', title, body, expandable: true, isPersistentPopup: false }); + // Show specialized friendly toasts if matched; otherwise show the generic one + const raw = (error?.response?.data) as any; + let rawString: string | undefined; + try { + if (typeof raw === 'string') rawString = raw; + else rawString = await normalizeAxiosErrorData(raw).then((d) => (typeof d === 'string' ? d : JSON.stringify(d))); + } catch { /* ignore */ } + const handled = showSpecialErrorToast(rawString, { status }); + if (!handled) { + alert({ alertType: 'error', title, body, expandable: true, isPersistentPopup: false }); + } return Promise.reject(error); } ); diff --git a/frontend/src/services/specialErrorToasts.ts b/frontend/src/services/specialErrorToasts.ts new file mode 100644 index 000000000..cdcc725fe --- /dev/null +++ b/frontend/src/services/specialErrorToasts.ts @@ -0,0 +1,57 @@ +import { alert } from '../components/toast'; + +interface ErrorToastMapping { + regex: RegExp; + i18nKey: string; + defaultMessage: string; +} + +// Centralized list of special backend error message patterns → friendly, translated toasts +const MAPPINGS: ErrorToastMapping[] = [ + { + regex: /pdf contains an encryption dictionary/i, + i18nKey: 'errors.encryptedPdfMustRemovePassword', + defaultMessage: 'This PDF is encrypted. Please unlock it using the Unlock PDF Forms tool.' + }, + { + regex: /the pdf document is passworded and either the password was not provided or was incorrect/i, + i18nKey: 'errors.incorrectPasswordProvided', + defaultMessage: 'The PDF password is incorrect or not provided.' + }, +]; + +function titleForStatus(status?: number): string { + if (!status) return 'Network error'; + if (status >= 500) return 'Server error'; + if (status >= 400) return 'Request error'; + return 'Request failed'; +} + +/** + * Match a raw backend error string against known patterns and show a friendly toast. + * Returns true if a special toast was shown, false otherwise. + */ +export function showSpecialErrorToast(rawError: string | undefined, options?: { status?: number }): boolean { + const message = (rawError || '').toString(); + if (!message) return false; + + for (const mapping of MAPPINGS) { + if (mapping.regex.test(message)) { + // Best-effort translation without hard dependency on i18n config + let body = mapping.defaultMessage; + try { + const anyGlobal: any = (globalThis as any); + const i18next = anyGlobal?.i18next; + if (i18next && typeof i18next.t === 'function') { + body = i18next.t(mapping.i18nKey, { defaultValue: mapping.defaultMessage }); + } + } catch { /* ignore translation errors */ } + const title = titleForStatus(options?.status); + alert({ alertType: 'error', title, body, expandable: true, isPersistentPopup: false }); + return true; + } + } + return false; +} + +