Sheets via neuer KeyboardAwareSheet-Composable (in Modal pattern, auto-grow mit Tastatur, paddingBottom-Lift): EditMail, AddDomain, CreateRoom, ConnectMail. GameOverScreen behält Spring-Slide-In, nutzt RN Keyboard.addListener für Lift. - KeyboardAwareSheet.tsx — universal modal with sheet-grow + keyboard-padding - react-native-keyboard-controller installiert + KeyboardProvider in Root - Snake: time + ScoreProgressBar + useSnakeSounds (haptic, audio TODO) - Tetris: title weg, Buttons zentriert, kein Pressable mit style-fn - DPad-Buttons 60→48, more bg, no scale - useMe: pub-sub listener pattern für app-weite avatar/nickname-Updates - dm.tsx: resolveAvatar wrap (iron.png-Warning) - Mail-error-humanizer + locales Recovery-Doc-Update in docs/internal/RECOVERY_LOG_2026-05-10.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
80 lines
2.0 KiB
TypeScript
80 lines
2.0 KiB
TypeScript
/**
|
|
* Übersetzt rohe Backend/IMAP-Fehlermeldungen in benutzerfreundliche Sätze.
|
|
*
|
|
* Backend liefert oft IMAP-Server-Antworten 1:1 durch (z.B.
|
|
* `[AUTHENTICATIONFAILED] Invalid credentials (Failure)`). Die zeigen wir
|
|
* dem User NICHT — stattdessen humane Übersetzung mit Hinweis was zu tun ist.
|
|
*/
|
|
export type MailErrorReason =
|
|
| 'auth_failed'
|
|
| 'app_password_required'
|
|
| 'connection_failed'
|
|
| 'host_unreachable'
|
|
| 'tls_error'
|
|
| 'rate_limited'
|
|
| 'unknown';
|
|
|
|
export function classifyMailError(raw: string | null | undefined): MailErrorReason {
|
|
if (!raw) return 'unknown';
|
|
const s = raw.toLowerCase();
|
|
|
|
if (
|
|
s.includes('authenticationfailed') ||
|
|
s.includes('invalid credentials') ||
|
|
s.includes('authentication failed') ||
|
|
s.includes('login failed') ||
|
|
s.includes('auth failed') ||
|
|
s.includes('bad password') ||
|
|
s.includes('wrong password')
|
|
) {
|
|
return 'auth_failed';
|
|
}
|
|
|
|
if (
|
|
s.includes('application-specific password') ||
|
|
s.includes('app password required') ||
|
|
s.includes('weblogin_required') ||
|
|
s.includes('two-factor')
|
|
) {
|
|
return 'app_password_required';
|
|
}
|
|
|
|
if (
|
|
s.includes('etimedout') ||
|
|
s.includes('econnrefused') ||
|
|
s.includes('connection timeout') ||
|
|
s.includes('socket timeout') ||
|
|
s.includes('connection reset')
|
|
) {
|
|
return 'connection_failed';
|
|
}
|
|
|
|
if (
|
|
s.includes('enotfound') ||
|
|
s.includes('host not found') ||
|
|
s.includes('getaddrinfo') ||
|
|
s.includes('dns')
|
|
) {
|
|
return 'host_unreachable';
|
|
}
|
|
|
|
if (s.includes('tls') || s.includes('ssl') || s.includes('certificate')) {
|
|
return 'tls_error';
|
|
}
|
|
|
|
if (s.includes('rate limit') || s.includes('too many') || s.includes('throttl')) {
|
|
return 'rate_limited';
|
|
}
|
|
|
|
return 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Liefert den i18n-Schlüssel für die humane Variante eines Mail-Errors.
|
|
* Caller ruft `t(humanizeMailError(rawError))` für den finalen Text.
|
|
*/
|
|
export function humanizeMailError(raw: string | null | undefined): string {
|
|
const reason = classifyMailError(raw);
|
|
return `mail.errors.${reason}`;
|
|
}
|