Notrufnummern-Datenbasis, Ländererkennung, State-Management und Lokalisierung (AP1-AP7)

Implementiert den kompletten Kern-Flow der Emergency App gemäß docs/plan.md:
- Notrufnummern-Datenbasis für 199 Länder + Modelle/Repository (AP1)
- DialerService für tel:-Notrufe ohne Auto-Dial (AP2)
- Statisches UI mit Panik-Button-Fallback (AP3)
- Priorisierte Ländererkennung: SIM/Netz -> GPS/Geocoding -> Cache (AP4)
- EmergencyProvider verdrahtet Repository/LocationService mit der UI (AP5)
- Manuelle Länderauswahl als Fallback samt eigener Priorität (AP6)
- UI-Lokalisierung (de/en/fr/es), an erkanntes Land gekoppelt (AP7)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
root
2026-07-24 11:29:38 +02:00
co-authored by Claude Sonnet 5
parent 40e95cc495
commit e3aa088bc2
54 changed files with 7550 additions and 145 deletions
+243
View File
@@ -0,0 +1,243 @@
import 'package:flutter/widgets.dart';
/// Ordnet ISO 3166-1 alpha-2 Ländercodes eine primäre Amts-/Mehrheitssprache
/// (ISO 639-1) zu (`docs/plan.md`, Abschnitt 2 + 8, AP7: "Lokalisierung:
/// Sprache an erkanntes Land gekoppelt").
///
/// **Bewusst pragmatisch, nicht für jedes Land 100% exakt:** Viele Länder
/// sind mehrsprachig (z. B. Belgien, Kamerun); hier wird jeweils die
/// praktisch sinnvollste/meistgesprochene Amtssprache gewählt. Deckt alle
/// Länder aus `assets/data/emergency_numbers.json` ab, ohne Anspruch auf
/// linguistische Vollständigkeit die App braucht daraus nur eine
/// plausible UI-Sprache, nicht eine sprachwissenschaftlich exakte Aussage.
///
/// Die meisten hier gelisteten Sprachen haben (Stand AP7) **keine** eigene
/// ARB-Übersetzung (nur `de`/`en`/`fr`/`es` existieren) das ist gewollt,
/// siehe [resolveAppLocale]: Nicht unterstützte Sprachen fallen automatisch
/// auf Englisch zurück.
const Map<String, String> countryLocaleMap = {
'AD': 'ca',
'AE': 'ar',
'AF': 'fa',
'AG': 'en',
'AL': 'sq',
'AM': 'hy',
'AO': 'pt',
'AR': 'es',
'AT': 'de',
'AU': 'en',
'AZ': 'az',
'BA': 'bs',
'BB': 'en',
'BD': 'bn',
'BE': 'nl',
'BF': 'fr',
'BG': 'bg',
'BH': 'ar',
'BI': 'rn',
'BJ': 'fr',
'BN': 'ms',
'BO': 'es',
'BR': 'pt',
'BS': 'en',
'BT': 'dz',
'BW': 'en',
'BY': 'be',
'BZ': 'en',
'CA': 'en',
'CD': 'fr',
'CF': 'fr',
'CG': 'fr',
'CH': 'de',
'CI': 'fr',
'CL': 'es',
'CM': 'fr',
'CN': 'zh',
'CO': 'es',
'CR': 'es',
'CU': 'es',
'CV': 'pt',
'CY': 'el',
'CZ': 'cs',
'DE': 'de',
'DJ': 'fr',
'DK': 'da',
'DM': 'en',
'DO': 'es',
'DZ': 'ar',
'EC': 'es',
'EE': 'et',
'EG': 'ar',
'ER': 'ti',
'ES': 'es',
'ET': 'am',
'FI': 'fi',
'FJ': 'en',
'FM': 'en',
'FR': 'fr',
'GA': 'fr',
'GB': 'en',
'GD': 'en',
'GE': 'ka',
'GH': 'en',
'GM': 'en',
'GN': 'fr',
'GQ': 'es',
'GR': 'el',
'GT': 'es',
'GW': 'pt',
'GY': 'en',
'HK': 'zh',
'HN': 'es',
'HR': 'hr',
'HT': 'fr',
'HU': 'hu',
'ID': 'id',
'IE': 'en',
'IL': 'he',
'IN': 'hi',
'IQ': 'ar',
'IR': 'fa',
'IS': 'is',
'IT': 'it',
'JM': 'en',
'JO': 'ar',
'JP': 'ja',
'KE': 'sw',
'KG': 'ky',
'KH': 'km',
'KI': 'en',
'KM': 'ar',
'KN': 'en',
'KP': 'ko',
'KR': 'ko',
'KW': 'ar',
'KZ': 'kk',
'LA': 'lo',
'LB': 'ar',
'LC': 'en',
'LI': 'de',
'LK': 'si',
'LR': 'en',
'LS': 'en',
'LT': 'lt',
'LU': 'fr',
'LV': 'lv',
'LY': 'ar',
'MA': 'ar',
'MC': 'fr',
'MD': 'ro',
'ME': 'sr',
'MG': 'fr',
'MH': 'en',
'MK': 'mk',
'ML': 'fr',
'MM': 'my',
'MN': 'mn',
'MO': 'zh',
'MR': 'ar',
'MT': 'mt',
'MU': 'en',
'MV': 'dv',
'MW': 'en',
'MX': 'es',
'MY': 'ms',
'MZ': 'pt',
'NA': 'en',
'NE': 'fr',
'NG': 'en',
'NI': 'es',
'NL': 'nl',
'NO': 'nb',
'NP': 'ne',
'NR': 'en',
'NZ': 'en',
'OM': 'ar',
'PA': 'es',
'PE': 'es',
'PG': 'en',
'PH': 'en',
'PK': 'ur',
'PL': 'pl',
'PS': 'ar',
'PT': 'pt',
'PW': 'en',
'PY': 'es',
'QA': 'ar',
'RO': 'ro',
'RS': 'sr',
'RU': 'ru',
'RW': 'rw',
'SA': 'ar',
'SB': 'en',
'SC': 'en',
'SD': 'ar',
'SE': 'sv',
'SG': 'en',
'SI': 'sl',
'SK': 'sk',
'SL': 'en',
'SM': 'it',
'SN': 'fr',
'SO': 'so',
'SR': 'nl',
'SS': 'en',
'ST': 'pt',
'SV': 'es',
'SY': 'ar',
'SZ': 'en',
'TD': 'fr',
'TG': 'fr',
'TH': 'th',
'TJ': 'tg',
'TL': 'pt',
'TM': 'tk',
'TN': 'ar',
'TO': 'en',
'TR': 'tr',
'TT': 'en',
'TV': 'en',
'TW': 'zh',
'TZ': 'sw',
'UA': 'uk',
'UG': 'en',
'US': 'en',
'UY': 'es',
'UZ': 'uz',
'VA': 'it',
'VC': 'en',
'VE': 'es',
'VN': 'vi',
'VU': 'en',
'WS': 'en',
'XK': 'sq',
'YE': 'ar',
'ZA': 'en',
'ZM': 'en',
'ZW': 'en',
};
/// Sprache, auf die zurückgefallen wird, wenn kein gemapptes/unterstütztes
/// Ergebnis ermittelbar ist. Muss immer als `AppLocalizations.supportedLocales`
/// vorhanden sein.
const _fallbackLanguageCode = 'en';
/// Berechnet die effektiv zu verwendende App-[Locale] aus einem (optionalen)
/// ISO 3166-1 alpha-2 Ländercode und der Liste tatsächlich unterstützter
/// Locales (`AppLocalizations.supportedLocales`).
///
/// Regeln (`docs/plan.md`, Abschnitt 2 + 8, AP7):
/// - [isoCode] ist `null` (Land noch nicht ermittelt/unbekannt) → Fallback
/// Englisch, kein Rätselraten über die Gerätesystemsprache.
/// - [isoCode] ist nicht in [countryLocaleMap] gelistet → Fallback Englisch.
/// - Die gemappte Sprache ist nicht in [supportedLocales] enthalten (also
/// keine ARB-Übersetzung vorhanden) → Fallback Englisch.
/// - Sonst: die gemappte Sprache.
Locale resolveAppLocale(String? isoCode, List<Locale> supportedLocales) {
final languageCode = isoCode == null ? null : countryLocaleMap[isoCode.toUpperCase()];
if (languageCode != null &&
supportedLocales.any((locale) => locale.languageCode == languageCode)) {
return Locale(languageCode);
}
return const Locale(_fallbackLanguageCode);
}