diff --git a/locales/rn/en.json b/locales/rn/en.json index 0f49d84c..1338e78e 100644 --- a/locales/rn/en.json +++ b/locales/rn/en.json @@ -105,6 +105,9 @@ "create": "Create", "dismiss": "Dismiss", "done": "Done", + "open": "Open", + "open_link_message": "This will open {host} outside the app.", + "open_link_title": "Open link?", "remove": "Remove", "retry": "Retry", "undo": "Undo", @@ -739,6 +742,20 @@ "push": { "confirm_disable_message_mobile": "This device will stop receiving new-mail alerts for this account.", "description_mobile": "Receive system notifications for new mail when the app is closed. Delivered via the Bulwark push relay; the relay never sees mail content.", + "err_android_only": "Push notifications are only available on Android.", + "err_fcm_empty": "Firebase returned an empty device token.", + "err_fcm_token": "Firebase could not issue a device token ({detail}). Push over FCM needs Google Play services on this device.", + "err_fcm_token_up": "Firebase could not issue a device token ({detail}). Push over FCM needs Google Play services on this device. This device has a UnifiedPush distributor installed - switch the delivery method to UnifiedPush instead.", + "err_jmap_refused": "The mail server refused the push subscription: {detail}", + "err_no_account": "No account loaded - cannot set up push.", + "err_permission": "Notification permission was not granted.", + "err_relay_https": "The relay URL must use https://.", + "err_relay_rejected": "The push relay rejected the registration: {detail}", + "err_relay_unreachable": "Could not reach the push relay at {url} ({detail}).", + "err_up_android_only": "UnifiedPush is only available on Android.", + "err_verify_rejected": "The mail server rejected the verification code: {detail}", + "err_verify_relay": "The relay never received the verification code from the mail server (last relay response: {detail}).", + "err_verify_timeout": "The mail server did not send a verification code to the relay within 75 s. Check that the server can reach the relay URL.", "phase_account": "Account", "phase_distributor": "UnifiedPush distributor", "phase_jmap": "Mail server subscription", diff --git a/src/lib/open-url.ts b/src/lib/open-url.ts index 4cd1aa74..5efd2a99 100644 Binary files a/src/lib/open-url.ts and b/src/lib/open-url.ts differ diff --git a/src/lib/push-background-task.ts b/src/lib/push-background-task.ts index 2684a190..dacd2778 100644 --- a/src/lib/push-background-task.ts +++ b/src/lib/push-background-task.ts @@ -472,7 +472,7 @@ async function processAccountForPush(accountId: string, payload: RelayPushData): const from = email.from?.[0]; const name = from?.name ?? ''; const address = from?.email ?? ''; - const title = name || address || 'New mail'; + const title = name || address || translate(locale, 'notifications.new_email', 'New email'); const body = email.subject || translate(locale, 'email_viewer.no_subject', '(No Subject)'); const initials = getEmailInitials(name, address); const bgColorHex = hslToHex(generateEmailAvatarColor(name, address)); diff --git a/src/lib/push-notifications.ts b/src/lib/push-notifications.ts index 93a713f5..e25a79b8 100644 --- a/src/lib/push-notifications.ts +++ b/src/lib/push-notifications.ts @@ -14,6 +14,7 @@ import { JMAPMethodError } from '../api/jmap-result'; import { CAPABILITIES } from '../api/types'; import type { EmailPushConfig, JMAPAccountInfo, Mailbox } from '../api/types'; import { generateAccountId } from './account-utils'; +import { t } from '../stores/locale-store'; import { getUnifiedPushDistributors, isUnifiedPushSupported, @@ -568,17 +569,23 @@ async function getFcmTokenOrThrow(native: BulwarkFcmNative): Promise { } catch (err) { const detail = err instanceof Error ? err.message : String(err); const distributors = await getUnifiedPushDistributors().catch(() => []); - const hint = - distributors.length > 0 - ? ' This device has a UnifiedPush distributor installed - switch the delivery method to UnifiedPush instead.' - : ''; throw new PushSetupError( 'token', - `Firebase could not issue a device token (${detail}). Push over FCM needs Google Play services on this device.${hint}`, + distributors.length > 0 + ? t( + 'settings.notifications.push.err_fcm_token_up', + 'Firebase could not issue a device token ({detail}). Push over FCM needs Google Play services on this device. This device has a UnifiedPush distributor installed - switch the delivery method to UnifiedPush instead.', + { detail }, + ) + : t( + 'settings.notifications.push.err_fcm_token', + 'Firebase could not issue a device token ({detail}). Push over FCM needs Google Play services on this device.', + { detail }, + ), ); } if (!token) { - throw new PushSetupError('token', 'Firebase returned an empty device token.'); + throw new PushSetupError('token', t('settings.notifications.push.err_fcm_empty', 'Firebase returned an empty device token.')); } return token; } @@ -655,10 +662,21 @@ async function registerWithRelay(params: { }); } catch (err) { const detail = err instanceof Error ? err.message : String(err); - throw new PushSetupError('relay', `Could not reach the push relay at ${params.relayBaseUrl} (${detail}).`); + throw new PushSetupError( + 'relay', + t('settings.notifications.push.err_relay_unreachable', 'Could not reach the push relay at {url} ({detail}).', { + url: params.relayBaseUrl, + detail, + }), + ); } if (!res.ok) { - throw new PushSetupError('relay', `The push relay rejected the registration: ${await readRelayError(res)}`); + throw new PushSetupError( + 'relay', + t('settings.notifications.push.err_relay_rejected', 'The push relay rejected the registration: {detail}', { + detail: await readRelayError(res), + }), + ); } } @@ -686,10 +704,21 @@ async function registerWithRelayUnifiedPush(params: { }); } catch (err) { const detail = err instanceof Error ? err.message : String(err); - throw new PushSetupError('relay', `Could not reach the push relay at ${params.relayBaseUrl} (${detail}).`); + throw new PushSetupError( + 'relay', + t('settings.notifications.push.err_relay_unreachable', 'Could not reach the push relay at {url} ({detail}).', { + url: params.relayBaseUrl, + detail, + }), + ); } if (!res.ok) { - throw new PushSetupError('relay', `The push relay rejected the registration: ${await readRelayError(res)}`); + throw new PushSetupError( + 'relay', + t('settings.notifications.push.err_relay_rejected', 'The push relay rejected the registration: {detail}', { + detail: await readRelayError(res), + }), + ); } } @@ -768,8 +797,15 @@ async function pollVerificationCode( throw new PushSetupError( 'verify', lastRelayError - ? `The relay never received the verification code from the mail server (last relay response: ${lastRelayError}).` - : 'The mail server did not send a verification code to the relay within 75 s. Check that the server can reach the relay URL.', + ? t( + 'settings.notifications.push.err_verify_relay', + 'The relay never received the verification code from the mail server (last relay response: {detail}).', + { detail: lastRelayError }, + ) + : t( + 'settings.notifications.push.err_verify_timeout', + 'The mail server did not send a verification code to the relay within 75 s. Check that the server can reach the relay URL.', + ), ); } @@ -812,22 +848,22 @@ async function setupPushNotificationsInner( const transport = await getEffectivePushTransport(); const native = getNative(); if (transport === 'fcm' && !native) { - throw new PushSetupError('platform', 'Push notifications are only available on Android.'); + throw new PushSetupError('platform', t('settings.notifications.push.err_android_only', 'Push notifications are only available on Android.')); } if (transport === 'unifiedpush' && !isUnifiedPushSupported()) { - throw new PushSetupError('platform', 'UnifiedPush is only available on Android.'); + throw new PushSetupError('platform', t('settings.notifications.push.err_up_android_only', 'UnifiedPush is only available on Android.')); } const relayBaseUrl = (params.relayBaseUrl ?? DEFAULT_RELAY_BASE_URL).replace(/\/+$/, ''); if (!relayBaseUrl) throw new PushSetupError('relay', 'relayBaseUrl is required'); if (!isValidRelayUrl(relayBaseUrl)) { - throw new PushSetupError('relay', 'The relay URL must use https://.'); + throw new PushSetupError('relay', t('settings.notifications.push.err_relay_https', 'The relay URL must use https://.')); } logPhase('permission'); const granted = await requestNotificationPermission(); if (!granted) { - throw new PushSetupError('permission', 'Notification permission was not granted.'); + throw new PushSetupError('permission', t('settings.notifications.push.err_permission', 'Notification permission was not granted.')); } logPhase('token', transport); @@ -844,7 +880,7 @@ async function setupPushNotificationsInner( const username = jmapClient.username; const serverUrl = jmapClient.serverUrl; if (!username || !serverUrl) { - throw new PushSetupError('account', 'No account loaded - cannot set up push.'); + throw new PushSetupError('account', t('settings.notifications.push.err_no_account', 'No account loaded - cannot set up push.')); } const accountId = generateAccountId(username, serverUrl); @@ -963,7 +999,10 @@ async function setupPushNotificationsInner( await rememberRefusedEmailPushAccounts(accountId, refusedBefore, emailPush, created.emailPush); } catch (err) { const detail = err instanceof Error ? err.message : String(err); - throw new PushSetupError('jmap', `The mail server refused the push subscription: ${detail}`); + throw new PushSetupError( + 'jmap', + t('settings.notifications.push.err_jmap_refused', 'The mail server refused the push subscription: {detail}', { detail }), + ); } logPhase('verify'); @@ -972,7 +1011,10 @@ async function setupPushNotificationsInner( await verifyPushSubscription(serverAssignedId, verificationCode); } catch (err) { const detail = err instanceof Error ? err.message : String(err); - throw new PushSetupError('verify', `The mail server rejected the verification code: ${detail}`); + throw new PushSetupError( + 'verify', + t('settings.notifications.push.err_verify_rejected', 'The mail server rejected the verification code: {detail}', { detail }), + ); } await AsyncStorage.setItem(subKey, serverAssignedId);