diff --git a/.gitignore b/.gitignore
index 3820a95..addfb96 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
*.pyc
*.swp
.DS_Store
+.sync_*.db
.atom/
.build/
.buildlog/
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index a5b6216..b7591f7 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,4 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/data/.gitkeep b/assets/data/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/assets/data/emergency_numbers.json b/assets/data/emergency_numbers.json
new file mode 100644
index 0000000..d9d8983
--- /dev/null
+++ b/assets/data/emergency_numbers.json
@@ -0,0 +1,4089 @@
+{
+ "AD": {
+ "flag": "🇦🇩",
+ "name_de": "Andorra",
+ "name_en": "Andorra",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "110",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "118",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "116",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "AE": {
+ "flag": "🇦🇪",
+ "name_de": "Vereinigte Arabische Emirate",
+ "name_en": "United Arab Emirates",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "999",
+ "type": "police"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "998",
+ "type": "ambulance"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "997",
+ "type": "fire"
+ }
+ ]
+ },
+ "AF": {
+ "flag": "🇦🇫",
+ "name_de": "Afghanistan",
+ "name_en": "Afghanistan",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "119",
+ "type": "police"
+ }
+ ]
+ },
+ "AG": {
+ "flag": "🇦🇬",
+ "name_de": "Antigua und Barbuda",
+ "name_en": "Antigua and Barbuda",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "AL": {
+ "flag": "🇦🇱",
+ "name_de": "Albanien",
+ "name_en": "Albania",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "129",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "128",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "127",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "AM": {
+ "flag": "🇦🇲",
+ "name_de": "Armenien",
+ "name_en": "Armenia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "102",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "101",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "103",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "AO": {
+ "flag": "🇦🇴",
+ "name_de": "Angola",
+ "name_en": "Angola",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "113",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "115",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "116",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "AR": {
+ "flag": "🇦🇷",
+ "name_de": "Argentinien",
+ "name_en": "Argentina",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "AT": {
+ "flag": "🇦🇹",
+ "name_de": "Österreich",
+ "name_en": "Austria",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "133",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "122",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "144",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "AU": {
+ "flag": "🇦🇺",
+ "name_de": "Australien",
+ "name_en": "Australia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "000",
+ "type": "general"
+ },
+ {
+ "label_de": "Notruf (Mobilfunk)",
+ "label_en": "Emergency (mobile)",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "AZ": {
+ "flag": "🇦🇿",
+ "name_de": "Aserbaidschan",
+ "name_en": "Azerbaijan",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "102",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "101",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "103",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "BA": {
+ "flag": "🇧🇦",
+ "name_de": "Bosnien und Herzegowina",
+ "name_en": "Bosnia and Herzegovina",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "122",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "123",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "124",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "BB": {
+ "flag": "🇧🇧",
+ "name_de": "Barbados",
+ "name_en": "Barbados",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "211",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "311",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "511",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "BD": {
+ "flag": "🇧🇩",
+ "name_de": "Bangladesch",
+ "name_en": "Bangladesh",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "999",
+ "type": "general"
+ }
+ ]
+ },
+ "BE": {
+ "flag": "🇧🇪",
+ "name_de": "Belgien",
+ "name_en": "Belgium",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "101",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "100",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "100",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "BF": {
+ "flag": "🇧🇫",
+ "name_de": "Burkina Faso",
+ "name_en": "Burkina Faso",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "17",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "18",
+ "type": "fire"
+ }
+ ]
+ },
+ "BG": {
+ "flag": "🇧🇬",
+ "name_de": "Bulgarien",
+ "name_en": "Bulgaria",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "BH": {
+ "flag": "🇧🇭",
+ "name_de": "Bahrain",
+ "name_en": "Bahrain",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "999",
+ "type": "general"
+ }
+ ]
+ },
+ "BI": {
+ "flag": "🇧🇮",
+ "name_de": "Burundi",
+ "name_en": "Burundi",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "117",
+ "type": "police"
+ }
+ ]
+ },
+ "BJ": {
+ "flag": "🇧🇯",
+ "name_de": "Benin",
+ "name_en": "Benin",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "117",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "118",
+ "type": "fire"
+ }
+ ]
+ },
+ "BN": {
+ "flag": "🇧🇳",
+ "name_de": "Brunei",
+ "name_en": "Brunei",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "993",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "995",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "991",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "BO": {
+ "flag": "🇧🇴",
+ "name_de": "Bolivien",
+ "name_en": "Bolivia",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "110",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "119",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "118",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "BR": {
+ "flag": "🇧🇷",
+ "name_de": "Brasilien",
+ "name_en": "Brazil",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "190",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "193",
+ "type": "fire"
+ },
+ {
+ "label_de": "SAMU",
+ "label_en": "SAMU",
+ "number": "192",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "BS": {
+ "flag": "🇧🇸",
+ "name_de": "Bahamas",
+ "name_en": "Bahamas",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "919",
+ "type": "general"
+ },
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "BT": {
+ "flag": "🇧🇹",
+ "name_de": "Bhutan",
+ "name_en": "Bhutan",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "113",
+ "type": "police"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "112",
+ "type": "ambulance"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "110",
+ "type": "fire"
+ }
+ ]
+ },
+ "BW": {
+ "flag": "🇧🇼",
+ "name_de": "Botsuana",
+ "name_en": "Botswana",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "999",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "998",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "997",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "BY": {
+ "flag": "🇧🇾",
+ "name_de": "Weißrussland",
+ "name_en": "Belarus",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "102",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "101",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "103",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "BZ": {
+ "flag": "🇧🇿",
+ "name_de": "Belize",
+ "name_en": "Belize",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "CA": {
+ "flag": "🇨🇦",
+ "name_de": "Kanada",
+ "name_en": "Canada",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "CD": {
+ "flag": "🇨🇩",
+ "name_de": "Demokratische Republik Kongo",
+ "name_en": "DR Congo",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "CF": {
+ "flag": "🇨🇫",
+ "name_de": "Zentralafrikanische Republik",
+ "name_en": "Central African Republic",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "117",
+ "type": "police"
+ }
+ ]
+ },
+ "CG": {
+ "flag": "🇨🇬",
+ "name_de": "Republik Kongo",
+ "name_en": "Republic of the Congo",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "117",
+ "type": "police"
+ }
+ ]
+ },
+ "CH": {
+ "flag": "🇨🇭",
+ "name_de": "Schweiz",
+ "name_en": "Switzerland",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "117",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "118",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "144",
+ "type": "ambulance"
+ },
+ {
+ "label_de": "Bergrettung",
+ "label_en": "Mountain Rescue",
+ "number": "1414",
+ "type": "mountain_rescue"
+ }
+ ]
+ },
+ "CI": {
+ "flag": "🇨🇮",
+ "name_de": "Elfenbeinküste",
+ "name_en": "Côte d'Ivoire",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "110",
+ "type": "police"
+ },
+ {
+ "label_de": "Gendarmerie",
+ "label_en": "Gendarmerie",
+ "number": "111",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "180",
+ "type": "fire"
+ }
+ ]
+ },
+ "CL": {
+ "flag": "🇨🇱",
+ "name_de": "Chile",
+ "name_en": "Chile",
+ "numbers": [
+ {
+ "label_de": "Carabineros",
+ "label_en": "Carabineros",
+ "number": "133",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "132",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "131",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "CM": {
+ "flag": "🇨🇲",
+ "name_de": "Kamerun",
+ "name_en": "Cameroon",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "117",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "118",
+ "type": "fire"
+ }
+ ]
+ },
+ "CN": {
+ "flag": "🇨🇳",
+ "name_de": "China",
+ "name_en": "China",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "110",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "119",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "120",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "CO": {
+ "flag": "🇨🇴",
+ "name_de": "Kolumbien",
+ "name_en": "Colombia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "123",
+ "type": "general"
+ }
+ ]
+ },
+ "CR": {
+ "flag": "🇨🇷",
+ "name_de": "Costa Rica",
+ "name_en": "Costa Rica",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "CU": {
+ "flag": "🇨🇺",
+ "name_de": "Kuba",
+ "name_en": "Cuba",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "106",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "105",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "104",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "CV": {
+ "flag": "🇨🇻",
+ "name_de": "Kap Verde",
+ "name_en": "Cabo Verde",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "132",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "131",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "130",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "CY": {
+ "flag": "🇨🇾",
+ "name_de": "Zypern",
+ "name_en": "Cyprus",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "199",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "199",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "199",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "CZ": {
+ "flag": "🇨🇿",
+ "name_de": "Tschechien",
+ "name_en": "Czechia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "158",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "150",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "155",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "DE": {
+ "flag": "🇩🇪",
+ "name_de": "Deutschland",
+ "name_en": "Germany",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "110",
+ "type": "police"
+ }
+ ]
+ },
+ "DJ": {
+ "flag": "🇩🇯",
+ "name_de": "Dschibuti",
+ "name_en": "Djibouti",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "17",
+ "type": "police"
+ }
+ ]
+ },
+ "DK": {
+ "flag": "🇩🇰",
+ "name_de": "Dänemark",
+ "name_en": "Denmark",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "DM": {
+ "flag": "🇩🇲",
+ "name_de": "Dominica",
+ "name_en": "Dominica",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "999",
+ "type": "general"
+ }
+ ]
+ },
+ "DO": {
+ "flag": "🇩🇴",
+ "name_de": "Dominikanische Republik",
+ "name_en": "Dominican Republic",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "DZ": {
+ "flag": "🇩🇿",
+ "name_de": "Algerien",
+ "name_en": "Algeria",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "17",
+ "type": "police"
+ },
+ {
+ "label_de": "Zivilschutz",
+ "label_en": "Civil Protection",
+ "number": "14",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "14",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "EC": {
+ "flag": "🇪🇨",
+ "name_de": "Ecuador",
+ "name_en": "Ecuador",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "EE": {
+ "flag": "🇪🇪",
+ "name_de": "Estland",
+ "name_en": "Estonia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "EG": {
+ "flag": "🇪🇬",
+ "name_de": "Ägypten",
+ "name_en": "Egypt",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "122",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "180",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "123",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "ER": {
+ "flag": "🇪🇷",
+ "name_de": "Eritrea",
+ "name_en": "Eritrea",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "113",
+ "type": "police"
+ }
+ ]
+ },
+ "ES": {
+ "flag": "🇪🇸",
+ "name_de": "Spanien",
+ "name_en": "Spain",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Nationalpolizei",
+ "label_en": "National Police",
+ "number": "091",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "080",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "061",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "ET": {
+ "flag": "🇪🇹",
+ "name_de": "Äthiopien",
+ "name_en": "Ethiopia",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "991",
+ "type": "police"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "907",
+ "type": "ambulance"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "939",
+ "type": "fire"
+ }
+ ]
+ },
+ "FI": {
+ "flag": "🇫🇮",
+ "name_de": "Finnland",
+ "name_en": "Finland",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "FJ": {
+ "flag": "🇫🇯",
+ "name_de": "Fidschi",
+ "name_en": "Fiji",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ },
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "917",
+ "type": "general"
+ }
+ ]
+ },
+ "FM": {
+ "flag": "🇫🇲",
+ "name_de": "Mikronesien",
+ "name_en": "Micronesia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "FR": {
+ "flag": "🇫🇷",
+ "name_de": "Frankreich",
+ "name_en": "France",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "17",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "18",
+ "type": "fire"
+ },
+ {
+ "label_de": "SAMU",
+ "label_en": "SAMU",
+ "number": "15",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "GA": {
+ "flag": "🇬🇦",
+ "name_de": "Gabun",
+ "name_en": "Gabon",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "1730",
+ "type": "police"
+ }
+ ]
+ },
+ "GB": {
+ "flag": "🇬🇧",
+ "name_de": "Vereinigtes Königreich",
+ "name_en": "United Kingdom",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "999",
+ "type": "general"
+ },
+ {
+ "label_de": "Notruf (Mobilfunk)",
+ "label_en": "Emergency (mobile)",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "GD": {
+ "flag": "🇬🇩",
+ "name_de": "Grenada",
+ "name_en": "Grenada",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "GE": {
+ "flag": "🇬🇪",
+ "name_de": "Georgien",
+ "name_en": "Georgia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "GH": {
+ "flag": "🇬🇭",
+ "name_de": "Ghana",
+ "name_en": "Ghana",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "191",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "192",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "193",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "GM": {
+ "flag": "🇬🇲",
+ "name_de": "Gambia",
+ "name_en": "Gambia",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "117",
+ "type": "police"
+ }
+ ]
+ },
+ "GN": {
+ "flag": "🇬🇳",
+ "name_de": "Guinea",
+ "name_en": "Guinea",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "117",
+ "type": "police"
+ }
+ ]
+ },
+ "GQ": {
+ "flag": "🇬🇶",
+ "name_de": "Äquatorialguinea",
+ "name_en": "Equatorial Guinea",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "114",
+ "type": "police"
+ }
+ ]
+ },
+ "GR": {
+ "flag": "🇬🇷",
+ "name_de": "Griechenland",
+ "name_en": "Greece",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "100",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "199",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "166",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "GT": {
+ "flag": "🇬🇹",
+ "name_de": "Guatemala",
+ "name_en": "Guatemala",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "110",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "122",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "128",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "GW": {
+ "flag": "🇬🇼",
+ "name_de": "Guinea-Bissau",
+ "name_en": "Guinea-Bissau",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "117",
+ "type": "police"
+ }
+ ]
+ },
+ "GY": {
+ "flag": "🇬🇾",
+ "name_de": "Guyana",
+ "name_en": "Guyana",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "911",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "912",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "913",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "HK": {
+ "flag": "🇭🇰",
+ "name_de": "Hongkong",
+ "name_en": "Hong Kong",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "999",
+ "type": "general"
+ }
+ ]
+ },
+ "HN": {
+ "flag": "🇭🇳",
+ "name_de": "Honduras",
+ "name_en": "Honduras",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "HR": {
+ "flag": "🇭🇷",
+ "name_de": "Kroatien",
+ "name_en": "Croatia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "192",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "193",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "194",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "HT": {
+ "flag": "🇭🇹",
+ "name_de": "Haiti",
+ "name_en": "Haiti",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "114",
+ "type": "police"
+ }
+ ]
+ },
+ "HU": {
+ "flag": "🇭🇺",
+ "name_de": "Ungarn",
+ "name_en": "Hungary",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "107",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "105",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "104",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "ID": {
+ "flag": "🇮🇩",
+ "name_de": "Indonesien",
+ "name_en": "Indonesia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "110",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "113",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "118",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "IE": {
+ "flag": "🇮🇪",
+ "name_de": "Irland",
+ "name_en": "Ireland",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "999",
+ "type": "general"
+ }
+ ]
+ },
+ "IL": {
+ "flag": "🇮🇱",
+ "name_de": "Israel",
+ "name_en": "Israel",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "100",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "102",
+ "type": "fire"
+ },
+ {
+ "label_de": "Magen David Adom",
+ "label_en": "Magen David Adom",
+ "number": "101",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "IN": {
+ "flag": "🇮🇳",
+ "name_de": "Indien",
+ "name_en": "India",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "IQ": {
+ "flag": "🇮🇶",
+ "name_de": "Irak",
+ "name_en": "Iraq",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "104",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "115",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "122",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "IR": {
+ "flag": "🇮🇷",
+ "name_de": "Iran",
+ "name_en": "Iran",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "110",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "125",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "115",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "IS": {
+ "flag": "🇮🇸",
+ "name_de": "Island",
+ "name_en": "Iceland",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "IT": {
+ "flag": "🇮🇹",
+ "name_de": "Italien",
+ "name_en": "Italy",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Staatspolizei",
+ "label_en": "State Police",
+ "number": "113",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "115",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "118",
+ "type": "ambulance"
+ },
+ {
+ "label_de": "Seenotrettung",
+ "label_en": "Sea Rescue",
+ "number": "1530",
+ "type": "sea_rescue"
+ }
+ ]
+ },
+ "JM": {
+ "flag": "🇯🇲",
+ "name_de": "Jamaika",
+ "name_en": "Jamaica",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "119",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "110",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "110",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "JO": {
+ "flag": "🇯🇴",
+ "name_de": "Jordanien",
+ "name_en": "Jordan",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "199",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "199",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "JP": {
+ "flag": "🇯🇵",
+ "name_de": "Japan",
+ "name_en": "Japan",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "110",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "119",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "119",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "KE": {
+ "flag": "🇰🇪",
+ "name_de": "Kenia",
+ "name_en": "Kenya",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "999",
+ "type": "general"
+ },
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "KG": {
+ "flag": "🇰🇬",
+ "name_de": "Kirgisistan",
+ "name_en": "Kyrgyzstan",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "102",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "101",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "103",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "KH": {
+ "flag": "🇰🇭",
+ "name_de": "Kambodscha",
+ "name_en": "Cambodia",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "117",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "118",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "119",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "KI": {
+ "flag": "🇰🇮",
+ "name_de": "Kiribati",
+ "name_en": "Kiribati",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "999",
+ "type": "police"
+ }
+ ]
+ },
+ "KM": {
+ "flag": "🇰🇲",
+ "name_de": "Komoren",
+ "name_en": "Comoros",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "17",
+ "type": "police"
+ }
+ ]
+ },
+ "KN": {
+ "flag": "🇰🇳",
+ "name_de": "St. Kitts und Nevis",
+ "name_en": "Saint Kitts and Nevis",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "KP": {
+ "flag": "🇰🇵",
+ "name_de": "Nordkorea",
+ "name_en": "North Korea",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "119",
+ "type": "police"
+ }
+ ]
+ },
+ "KR": {
+ "flag": "🇰🇷",
+ "name_de": "Südkorea",
+ "name_en": "South Korea",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "112",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "119",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "119",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "KW": {
+ "flag": "🇰🇼",
+ "name_de": "Kuwait",
+ "name_en": "Kuwait",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "KZ": {
+ "flag": "🇰🇿",
+ "name_de": "Kasachstan",
+ "name_en": "Kazakhstan",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "102",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "101",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "103",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "LA": {
+ "flag": "🇱🇦",
+ "name_de": "Laos",
+ "name_en": "Laos",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "191",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "190",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "195",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "LB": {
+ "flag": "🇱🇧",
+ "name_de": "Libanon",
+ "name_en": "Lebanon",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "112",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "175",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rotes Kreuz",
+ "label_en": "Red Cross",
+ "number": "140",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "LC": {
+ "flag": "🇱🇨",
+ "name_de": "St. Lucia",
+ "name_en": "Saint Lucia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "999",
+ "type": "general"
+ }
+ ]
+ },
+ "LI": {
+ "flag": "🇱🇮",
+ "name_de": "Liechtenstein",
+ "name_en": "Liechtenstein",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "117",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "118",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "144",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "LK": {
+ "flag": "🇱🇰",
+ "name_de": "Sri Lanka",
+ "name_en": "Sri Lanka",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "119",
+ "type": "police"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "1990",
+ "type": "ambulance"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "110",
+ "type": "fire"
+ }
+ ]
+ },
+ "LR": {
+ "flag": "🇱🇷",
+ "name_de": "Liberia",
+ "name_en": "Liberia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "LS": {
+ "flag": "🇱🇸",
+ "name_de": "Lesotho",
+ "name_en": "Lesotho",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "123",
+ "type": "police"
+ }
+ ]
+ },
+ "LT": {
+ "flag": "🇱🇹",
+ "name_de": "Litauen",
+ "name_en": "Lithuania",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "LU": {
+ "flag": "🇱🇺",
+ "name_de": "Luxemburg",
+ "name_en": "Luxembourg",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "113",
+ "type": "police"
+ }
+ ]
+ },
+ "LV": {
+ "flag": "🇱🇻",
+ "name_de": "Lettland",
+ "name_en": "Latvia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "LY": {
+ "flag": "🇱🇾",
+ "name_de": "Libyen",
+ "name_en": "Libya",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "1515",
+ "type": "police"
+ }
+ ]
+ },
+ "MA": {
+ "flag": "🇲🇦",
+ "name_de": "Marokko",
+ "name_en": "Morocco",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "19",
+ "type": "police"
+ },
+ {
+ "label_de": "Gendarmerie",
+ "label_en": "Gendarmerie",
+ "number": "177",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "15",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "15",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "MC": {
+ "flag": "🇲🇨",
+ "name_de": "Monaco",
+ "name_en": "Monaco",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "17",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "18",
+ "type": "fire"
+ }
+ ]
+ },
+ "MD": {
+ "flag": "🇲🇩",
+ "name_de": "Moldau",
+ "name_en": "Moldova",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "ME": {
+ "flag": "🇲🇪",
+ "name_de": "Montenegro",
+ "name_en": "Montenegro",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "122",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "123",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "124",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "MG": {
+ "flag": "🇲🇬",
+ "name_de": "Madagaskar",
+ "name_en": "Madagascar",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "117",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "118",
+ "type": "fire"
+ }
+ ]
+ },
+ "MH": {
+ "flag": "🇲🇭",
+ "name_de": "Marshallinseln",
+ "name_en": "Marshall Islands",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "MK": {
+ "flag": "🇲🇰",
+ "name_de": "Nordmazedonien",
+ "name_en": "North Macedonia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "192",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "193",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "194",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "ML": {
+ "flag": "🇲🇱",
+ "name_de": "Mali",
+ "name_en": "Mali",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "17",
+ "type": "police"
+ }
+ ]
+ },
+ "MM": {
+ "flag": "🇲🇲",
+ "name_de": "Myanmar",
+ "name_en": "Myanmar",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "199",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "191",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "192",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "MN": {
+ "flag": "🇲🇳",
+ "name_de": "Mongolei",
+ "name_en": "Mongolia",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "102",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "101",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "103",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "MO": {
+ "flag": "🇲🇴",
+ "name_de": "Macau",
+ "name_en": "Macau",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "999",
+ "type": "general"
+ }
+ ]
+ },
+ "MR": {
+ "flag": "🇲🇷",
+ "name_de": "Mauretanien",
+ "name_en": "Mauritania",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "17",
+ "type": "police"
+ }
+ ]
+ },
+ "MT": {
+ "flag": "🇲🇹",
+ "name_de": "Malta",
+ "name_en": "Malta",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "MU": {
+ "flag": "🇲🇺",
+ "name_de": "Mauritius",
+ "name_en": "Mauritius",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "999",
+ "type": "general"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "995",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "114",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "MV": {
+ "flag": "🇲🇻",
+ "name_de": "Malediven",
+ "name_en": "Maldives",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "119",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "118",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "102",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "MW": {
+ "flag": "🇲🇼",
+ "name_de": "Malawi",
+ "name_en": "Malawi",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "997",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "999",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "998",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "MX": {
+ "flag": "🇲🇽",
+ "name_de": "Mexiko",
+ "name_en": "Mexico",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "MY": {
+ "flag": "🇲🇾",
+ "name_de": "Malaysia",
+ "name_en": "Malaysia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "999",
+ "type": "general"
+ },
+ {
+ "label_de": "Notruf (Mobilfunk)",
+ "label_en": "Emergency (mobile)",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "MZ": {
+ "flag": "🇲🇿",
+ "name_de": "Mosambik",
+ "name_en": "Mozambique",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "119",
+ "type": "police"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "117",
+ "type": "ambulance"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "198",
+ "type": "fire"
+ }
+ ]
+ },
+ "NA": {
+ "flag": "🇳🇦",
+ "name_de": "Namibia",
+ "name_en": "Namibia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "10111",
+ "type": "police"
+ }
+ ]
+ },
+ "NE": {
+ "flag": "🇳🇪",
+ "name_de": "Niger",
+ "name_en": "Niger",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "17",
+ "type": "police"
+ }
+ ]
+ },
+ "NG": {
+ "flag": "🇳🇬",
+ "name_de": "Nigeria",
+ "name_en": "Nigeria",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "NI": {
+ "flag": "🇳🇮",
+ "name_de": "Nicaragua",
+ "name_en": "Nicaragua",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "NL": {
+ "flag": "🇳🇱",
+ "name_de": "Niederlande",
+ "name_en": "Netherlands",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "NO": {
+ "flag": "🇳🇴",
+ "name_de": "Norwegen",
+ "name_en": "Norway",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "112",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "110",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "113",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "NP": {
+ "flag": "🇳🇵",
+ "name_de": "Nepal",
+ "name_en": "Nepal",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "100",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "101",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "102",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "NR": {
+ "flag": "🇳🇷",
+ "name_de": "Nauru",
+ "name_en": "Nauru",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "110",
+ "type": "police"
+ }
+ ]
+ },
+ "NZ": {
+ "flag": "🇳🇿",
+ "name_de": "Neuseeland",
+ "name_en": "New Zealand",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "111",
+ "type": "general"
+ }
+ ]
+ },
+ "OM": {
+ "flag": "🇴🇲",
+ "name_de": "Oman",
+ "name_en": "Oman",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "9999",
+ "type": "general"
+ }
+ ]
+ },
+ "PA": {
+ "flag": "🇵🇦",
+ "name_de": "Panama",
+ "name_en": "Panama",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "104",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "103",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "911",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "PE": {
+ "flag": "🇵🇪",
+ "name_de": "Peru",
+ "name_en": "Peru",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "105",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "116",
+ "type": "fire"
+ },
+ {
+ "label_de": "SAMU",
+ "label_en": "SAMU",
+ "number": "106",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "PG": {
+ "flag": "🇵🇬",
+ "name_de": "Papua-Neuguinea",
+ "name_en": "Papua New Guinea",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "000",
+ "type": "general"
+ },
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "PH": {
+ "flag": "🇵🇭",
+ "name_de": "Philippinen",
+ "name_en": "Philippines",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "PK": {
+ "flag": "🇵🇰",
+ "name_de": "Pakistan",
+ "name_en": "Pakistan",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "15",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "16",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "1122",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "PL": {
+ "flag": "🇵🇱",
+ "name_de": "Polen",
+ "name_en": "Poland",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "997",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "998",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "999",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "PS": {
+ "flag": "🇵🇸",
+ "name_de": "Palästina",
+ "name_en": "Palestine",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "100",
+ "type": "police"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "101",
+ "type": "ambulance"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "102",
+ "type": "fire"
+ }
+ ]
+ },
+ "PT": {
+ "flag": "🇵🇹",
+ "name_de": "Portugal",
+ "name_en": "Portugal",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "PW": {
+ "flag": "🇵🇼",
+ "name_de": "Palau",
+ "name_en": "Palau",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "PY": {
+ "flag": "🇵🇾",
+ "name_de": "Paraguay",
+ "name_en": "Paraguay",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "132",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "141",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "QA": {
+ "flag": "🇶🇦",
+ "name_de": "Katar",
+ "name_en": "Qatar",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "999",
+ "type": "general"
+ }
+ ]
+ },
+ "RO": {
+ "flag": "🇷🇴",
+ "name_de": "Rumänien",
+ "name_en": "Romania",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "RS": {
+ "flag": "🇷🇸",
+ "name_de": "Serbien",
+ "name_en": "Serbia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "192",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "193",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "194",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "RU": {
+ "flag": "🇷🇺",
+ "name_de": "Russland",
+ "name_en": "Russia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "102",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "101",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "103",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "RW": {
+ "flag": "🇷🇼",
+ "name_de": "Ruanda",
+ "name_en": "Rwanda",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "SA": {
+ "flag": "🇸🇦",
+ "name_de": "Saudi-Arabien",
+ "name_en": "Saudi Arabia",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "999",
+ "type": "police"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "997",
+ "type": "ambulance"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "998",
+ "type": "fire"
+ }
+ ]
+ },
+ "SB": {
+ "flag": "🇸🇧",
+ "name_de": "Salomonen",
+ "name_en": "Solomon Islands",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "999",
+ "type": "police"
+ }
+ ]
+ },
+ "SC": {
+ "flag": "🇸🇨",
+ "name_de": "Seychellen",
+ "name_en": "Seychelles",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "999",
+ "type": "general"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "151",
+ "type": "ambulance"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "133",
+ "type": "fire"
+ }
+ ]
+ },
+ "SD": {
+ "flag": "🇸🇩",
+ "name_de": "Sudan",
+ "name_en": "Sudan",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "999",
+ "type": "police"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "999",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "SE": {
+ "flag": "🇸🇪",
+ "name_de": "Schweden",
+ "name_en": "Sweden",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "SG": {
+ "flag": "🇸🇬",
+ "name_de": "Singapur",
+ "name_en": "Singapore",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "999",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "995",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "995",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "SI": {
+ "flag": "🇸🇮",
+ "name_de": "Slowenien",
+ "name_en": "Slovenia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "113",
+ "type": "police"
+ }
+ ]
+ },
+ "SK": {
+ "flag": "🇸🇰",
+ "name_de": "Slowakei",
+ "name_en": "Slovakia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "158",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "150",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "155",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "SL": {
+ "flag": "🇸🇱",
+ "name_de": "Sierra Leone",
+ "name_en": "Sierra Leone",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "999",
+ "type": "general"
+ }
+ ]
+ },
+ "SM": {
+ "flag": "🇸🇲",
+ "name_de": "San Marino",
+ "name_en": "San Marino",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "113",
+ "type": "general"
+ }
+ ]
+ },
+ "SN": {
+ "flag": "🇸🇳",
+ "name_de": "Senegal",
+ "name_en": "Senegal",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "17",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "18",
+ "type": "fire"
+ }
+ ]
+ },
+ "SO": {
+ "flag": "🇸🇴",
+ "name_de": "Somalia",
+ "name_en": "Somalia",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "888",
+ "type": "general"
+ }
+ ]
+ },
+ "SR": {
+ "flag": "🇸🇷",
+ "name_de": "Suriname",
+ "name_en": "Suriname",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "115",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "110",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "113",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "SS": {
+ "flag": "🇸🇸",
+ "name_de": "Südsudan",
+ "name_en": "South Sudan",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "777",
+ "type": "police"
+ }
+ ]
+ },
+ "ST": {
+ "flag": "🇸🇹",
+ "name_de": "São Tomé und Príncipe",
+ "name_en": "São Tomé and Príncipe",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "SV": {
+ "flag": "🇸🇻",
+ "name_de": "El Salvador",
+ "name_en": "El Salvador",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "SY": {
+ "flag": "🇸🇾",
+ "name_de": "Syrien",
+ "name_en": "Syria",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "112",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "113",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "110",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "SZ": {
+ "flag": "🇸🇿",
+ "name_de": "Eswatini",
+ "name_en": "Eswatini",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "999",
+ "type": "police"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "977",
+ "type": "ambulance"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "933",
+ "type": "fire"
+ }
+ ]
+ },
+ "TD": {
+ "flag": "🇹🇩",
+ "name_de": "Tschad",
+ "name_en": "Chad",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "17",
+ "type": "police"
+ }
+ ]
+ },
+ "TG": {
+ "flag": "🇹🇬",
+ "name_de": "Togo",
+ "name_en": "Togo",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "117",
+ "type": "police"
+ }
+ ]
+ },
+ "TH": {
+ "flag": "🇹🇭",
+ "name_de": "Thailand",
+ "name_en": "Thailand",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "191",
+ "type": "police"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "1669",
+ "type": "ambulance"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "199",
+ "type": "fire"
+ }
+ ]
+ },
+ "TJ": {
+ "flag": "🇹🇯",
+ "name_de": "Tadschikistan",
+ "name_en": "Tajikistan",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "102",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "101",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "103",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "TL": {
+ "flag": "🇹🇱",
+ "name_de": "Osttimor",
+ "name_en": "Timor-Leste",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "TM": {
+ "flag": "🇹🇲",
+ "name_de": "Turkmenistan",
+ "name_en": "Turkmenistan",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "02",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "01",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "03",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "TN": {
+ "flag": "🇹🇳",
+ "name_de": "Tunesien",
+ "name_en": "Tunisia",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "197",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "198",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "190",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "TO": {
+ "flag": "🇹🇴",
+ "name_de": "Tonga",
+ "name_en": "Tonga",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "922",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "911",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "933",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "TR": {
+ "flag": "🇹🇷",
+ "name_de": "Türkei",
+ "name_en": "Turkey",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "155",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "110",
+ "type": "fire"
+ },
+ {
+ "label_de": "Jandarma",
+ "label_en": "Gendarmerie",
+ "number": "156",
+ "type": "police"
+ }
+ ]
+ },
+ "TT": {
+ "flag": "🇹🇹",
+ "name_de": "Trinidad und Tobago",
+ "name_en": "Trinidad and Tobago",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "999",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "990",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "990",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "TV": {
+ "flag": "🇹🇻",
+ "name_de": "Tuvalu",
+ "name_en": "Tuvalu",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "TW": {
+ "flag": "🇹🇼",
+ "name_de": "Taiwan",
+ "name_en": "Taiwan",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "110",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "119",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "119",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "TZ": {
+ "flag": "🇹🇿",
+ "name_de": "Tansania",
+ "name_en": "Tanzania",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "999",
+ "type": "general"
+ }
+ ]
+ },
+ "UA": {
+ "flag": "🇺🇦",
+ "name_de": "Ukraine",
+ "name_en": "Ukraine",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "102",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "101",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "103",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "UG": {
+ "flag": "🇺🇬",
+ "name_de": "Uganda",
+ "name_en": "Uganda",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "999",
+ "type": "general"
+ },
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "US": {
+ "flag": "🇺🇸",
+ "name_de": "Vereinigte Staaten",
+ "name_en": "United States",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "UY": {
+ "flag": "🇺🇾",
+ "name_de": "Uruguay",
+ "name_en": "Uruguay",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "104",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "105",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "UZ": {
+ "flag": "🇺🇿",
+ "name_de": "Usbekistan",
+ "name_en": "Uzbekistan",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "102",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "101",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "103",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "VA": {
+ "flag": "🇻🇦",
+ "name_de": "Vatikanstadt",
+ "name_en": "Vatican City",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ }
+ ]
+ },
+ "VC": {
+ "flag": "🇻🇨",
+ "name_de": "St. Vincent und die Grenadinen",
+ "name_en": "Saint Vincent and the Grenadines",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "VE": {
+ "flag": "🇻🇪",
+ "name_de": "Venezuela",
+ "name_en": "Venezuela",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "VN": {
+ "flag": "🇻🇳",
+ "name_de": "Vietnam",
+ "name_en": "Vietnam",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "113",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "114",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "115",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "VU": {
+ "flag": "🇻🇺",
+ "name_de": "Vanuatu",
+ "name_en": "Vanuatu",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "911",
+ "type": "general"
+ }
+ ]
+ },
+ "WS": {
+ "flag": "🇼🇸",
+ "name_de": "Samoa",
+ "name_en": "Samoa",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "911",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "994",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "996",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "XK": {
+ "flag": "🇽🇰",
+ "name_de": "Kosovo",
+ "name_en": "Kosovo",
+ "numbers": [
+ {
+ "label_de": "Notruf",
+ "label_en": "Emergency",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "192",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "193",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "194",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "YE": {
+ "flag": "🇾🇪",
+ "name_de": "Jemen",
+ "name_en": "Yemen",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "194",
+ "type": "police"
+ }
+ ]
+ },
+ "ZA": {
+ "flag": "🇿🇦",
+ "name_de": "Südafrika",
+ "name_en": "South Africa",
+ "numbers": [
+ {
+ "label_de": "Notruf (Mobilfunk)",
+ "label_en": "Emergency (mobile)",
+ "number": "112",
+ "type": "general"
+ },
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "10111",
+ "type": "police"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "10177",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "ZM": {
+ "flag": "🇿🇲",
+ "name_de": "Sambia",
+ "name_en": "Zambia",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "999",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "993",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "992",
+ "type": "ambulance"
+ }
+ ]
+ },
+ "ZW": {
+ "flag": "🇿🇼",
+ "name_de": "Simbabwe",
+ "name_en": "Zimbabwe",
+ "numbers": [
+ {
+ "label_de": "Polizei",
+ "label_en": "Police",
+ "number": "995",
+ "type": "police"
+ },
+ {
+ "label_de": "Feuerwehr",
+ "label_en": "Fire Brigade",
+ "number": "993",
+ "type": "fire"
+ },
+ {
+ "label_de": "Rettungsdienst",
+ "label_en": "Ambulance",
+ "number": "994",
+ "type": "ambulance"
+ }
+ ]
+ }
+}
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 3933fc3..9974849 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -24,6 +24,12 @@
????
CFBundleVersion
$(FLUTTER_BUILD_NUMBER)
+ LSApplicationQueriesSchemes
+
+ tel
+
+ NSLocationWhenInUseUsageDescription
+ Wird verwendet, um die passende Notrufnummer für dein aktuelles Land anzuzeigen.
LSRequiresIPhoneOS
UIApplicationSceneManifest
diff --git a/lib/app.dart b/lib/app.dart
new file mode 100644
index 0000000..1d431f4
--- /dev/null
+++ b/lib/app.dart
@@ -0,0 +1,56 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+import 'core/country_locale_map.dart';
+import 'features/home/home_screen.dart';
+import 'l10n/app_localizations.dart';
+import 'providers/emergency_provider.dart';
+import 'repositories/emergency_number_repository.dart';
+import 'services/location_service.dart';
+
+/// Wurzel-Widget der App.
+///
+/// [repository] und [locationService] sind injizierbar, damit Tests eigene
+/// (ggf. gefakte) Instanzen für den intern erzeugten [EmergencyProvider]
+/// verwenden können, statt echte Asset-I/O bzw. SIM/GPS/Cache-Zugriffe
+/// anzustoßen. Wird stattdessen direkt ein fertiger [provider] übergeben,
+/// werden [repository]/[locationService] ignoriert – das erlaubt Tests, den
+/// `EmergencyProvider` selbst zu faken bzw. vorab zu befüllen.
+class App extends StatelessWidget {
+ const App({super.key, this.repository, this.locationService, this.provider});
+
+ final EmergencyNumberRepository? repository;
+ final LocationService? locationService;
+ final EmergencyProvider? provider;
+
+ @override
+ Widget build(BuildContext context) {
+ return ChangeNotifierProvider(
+ create: (_) =>
+ provider ??
+ EmergencyProvider(
+ repository: repository ?? EmergencyNumberRepository(),
+ locationService: locationService ?? LocationService(),
+ ),
+ // AP7 (Lokalisierung): `locale` wird reaktiv aus dem erkannten Land
+ // berechnet (docs/plan.md, Abschnitt 2 + 8), daher ein Consumer statt
+ // eines einmalig gebauten MaterialApp – ändert sich `country` (z. B.
+ // nach manueller Länderauswahl aus AP6), rebuildet auch die App-Sprache.
+ child: Consumer(
+ builder: (context, emergencyProvider, _) {
+ return MaterialApp(
+ title: 'Emergency',
+ theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.red)),
+ localizationsDelegates: AppLocalizations.localizationsDelegates,
+ supportedLocales: AppLocalizations.supportedLocales,
+ locale: resolveAppLocale(
+ emergencyProvider.country?.isoCode,
+ AppLocalizations.supportedLocales,
+ ),
+ home: const HomeScreen(),
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/lib/core/country_locale_map.dart b/lib/core/country_locale_map.dart
new file mode 100644
index 0000000..a6a7d9a
--- /dev/null
+++ b/lib/core/country_locale_map.dart
@@ -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 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 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);
+}
diff --git a/lib/features/home/country_picker_screen.dart b/lib/features/home/country_picker_screen.dart
new file mode 100644
index 0000000..dbeb28b
--- /dev/null
+++ b/lib/features/home/country_picker_screen.dart
@@ -0,0 +1,93 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+import '../../l10n/app_localizations.dart';
+import '../../models/country.dart';
+import '../../providers/emergency_provider.dart';
+
+/// Durchsuchbare Länderliste zur manuellen Länderauswahl
+/// (`docs/plan.md`, Abschnitt 7 + 8, AP6).
+///
+/// Tap auf ein Land ruft [EmergencyProvider.selectCountryManually] auf –
+/// das persistiert die Auswahl und stößt die Ermittlungskette danach sofort
+/// erneut an – und schließt den Screen.
+class CountryPickerScreen extends StatefulWidget {
+ const CountryPickerScreen({super.key});
+
+ @override
+ State createState() => _CountryPickerScreenState();
+}
+
+class _CountryPickerScreenState extends State {
+ final _searchController = TextEditingController();
+ String _query = '';
+
+ @override
+ void dispose() {
+ _searchController.dispose();
+ super.dispose();
+ }
+
+ List _filtered(List countries) {
+ final query = _query.trim().toLowerCase();
+ if (query.isEmpty) return countries;
+ return countries
+ .where(
+ (country) =>
+ country.nameDe.toLowerCase().contains(query) ||
+ country.nameEn.toLowerCase().contains(query) ||
+ country.isoCode.toLowerCase().contains(query),
+ )
+ .toList(growable: false);
+ }
+
+ Future _select(BuildContext context, Country country) async {
+ await context.read().selectCountryManually(country.isoCode);
+ if (context.mounted) Navigator.of(context).pop();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final repository = context.read().repository;
+ final countries = _filtered(repository.allCountries);
+ final l10n = AppLocalizations.of(context)!;
+
+ return Scaffold(
+ appBar: AppBar(title: Text(l10n.pickerTitle)),
+ body: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(16),
+ child: TextField(
+ controller: _searchController,
+ autofocus: true,
+ decoration: InputDecoration(
+ prefixIcon: const Icon(Icons.search),
+ hintText: l10n.pickerSearchHint,
+ border: const OutlineInputBorder(),
+ ),
+ onChanged: (value) => setState(() => _query = value),
+ ),
+ ),
+ Expanded(
+ child: countries.isEmpty
+ ? Center(child: Text(l10n.pickerNoResults))
+ : ListView.builder(
+ itemCount: countries.length,
+ itemBuilder: (context, index) {
+ final country = countries[index];
+ return ListTile(
+ key: ValueKey(country.isoCode),
+ leading: Text(country.flag, style: const TextStyle(fontSize: 24)),
+ title: Text(country.nameDe),
+ subtitle: Text(country.isoCode),
+ onTap: () => _select(context, country),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/features/home/home_screen.dart b/lib/features/home/home_screen.dart
new file mode 100644
index 0000000..550fa18
--- /dev/null
+++ b/lib/features/home/home_screen.dart
@@ -0,0 +1,231 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+import '../../l10n/app_localizations.dart';
+import '../../models/country.dart';
+import '../../models/emergency_number.dart';
+import '../../models/emergency_type.dart';
+import '../../providers/emergency_provider.dart';
+import '../../providers/emergency_status.dart';
+import 'country_picker_screen.dart';
+import 'widgets/country_header.dart';
+import 'widgets/emergency_button.dart';
+
+/// Startbildschirm mit Länder-Header und Notruf-Buttons.
+///
+/// AP5 (State-Integration): Land und Ladezustand kommen jetzt aus dem
+/// [EmergencyProvider] (SIM/GPS/Cache-Ermittlungskette aus AP4), nicht mehr
+/// aus einem hartkodierten ISO-Code (AP3).
+class HomeScreen extends StatefulWidget {
+ const HomeScreen({super.key});
+
+ @override
+ State createState() => _HomeScreenState();
+}
+
+class _HomeScreenState extends State {
+ @override
+ void initState() {
+ super.initState();
+ // Einmaliger Start der Ermittlungskette nach dem ersten Frame: init()
+ // ruft synchron (vor dem ersten await) notifyListeners() auf, was
+ // während der laufenden Baumkonstruktion (initState) zu "setState()
+ // called during build" führen würde – daher erst im Post-Frame-Callback.
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted) context.read().init();
+ });
+ }
+
+ /// Globaler Panikmodus-Button (Plan Abschnitt 2 + 6): 112 funktioniert als
+ /// GSM-weiter Fallback in den meisten Mobilfunknetzen und muss unabhängig
+ /// vom Lade-/Fehlerzustand der Ländererkennung immer erreichbar sein.
+ static const _panicNumber = EmergencyNumber(
+ type: EmergencyType.general,
+ number: '112',
+ labelDe: 'Internationaler Notruf',
+ labelEn: 'International Emergency',
+ );
+
+ /// True, wenn das erkannte Land bereits eine `general`-Nummer "112" führt
+ /// (z. B. DE) – dann würde der Panik-Button dieselbe Nummer doppelt zeigen.
+ bool _countryAlreadyHasPanicNumber(Country country) {
+ return country.numbers.any(
+ (n) => n.type == EmergencyType.general && n.number == _panicNumber.number,
+ );
+ }
+
+ bool _showPanicButton(EmergencyProvider provider) {
+ final country = provider.country;
+ if (provider.status == EmergencyStatus.ready && country != null) {
+ return !_countryAlreadyHasPanicNumber(country);
+ }
+ // Ladezustand, Fehler, countryUnknown oder Land nicht in Datenbasis:
+ // Panik-Button ist die einzig sichere Konstante, also immer sichtbar.
+ return true;
+ }
+
+ /// Öffnet den `CountryPickerScreen` (Plan Abschnitt 7 + 8, AP6: manuelle
+ /// Länderauswahl als Fallback, wenn das Land nicht automatisch ermittelbar
+ /// ist bzw. jederzeit als bewusste Nutzerentscheidung).
+ void _openCountryPicker(BuildContext context) {
+ Navigator.of(context).push(MaterialPageRoute(builder: (_) => const CountryPickerScreen()));
+ }
+
+ Widget _buildContent(BuildContext context, EmergencyProvider provider) {
+ final l10n = AppLocalizations.of(context)!;
+
+ switch (provider.status) {
+ case EmergencyStatus.initializing:
+ case EmergencyStatus.locating:
+ return const Center(child: CircularProgressIndicator());
+
+ case EmergencyStatus.error:
+ return _StatusMessage(
+ message: provider.errorMessage ?? l10n.errorGeneric,
+ retryLabel: l10n.retryButton,
+ selectCountryLabel: l10n.selectCountryButton,
+ onRetry: () => context.read().retry(),
+ onSelectCountry: () => _openCountryPicker(context),
+ );
+
+ // permissionDenied wird bewusst wie countryUnknown behandelt, siehe
+ // Doc-Kommentar in EmergencyProvider.
+ case EmergencyStatus.permissionDenied:
+ case EmergencyStatus.countryUnknown:
+ return _StatusMessage(
+ message: l10n.countryUnknownMessage,
+ retryLabel: l10n.retryButton,
+ selectCountryLabel: l10n.selectCountryButton,
+ onRetry: () => context.read().retry(),
+ onSelectCountry: () => _openCountryPicker(context),
+ );
+
+ case EmergencyStatus.ready:
+ final country = provider.country;
+ if (country == null) {
+ return _StatusMessage(message: l10n.countryNotInDatabaseMessage);
+ }
+ return _CountryContent(country: country);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final provider = context.watch();
+ final l10n = AppLocalizations.of(context)!;
+
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(l10n.appTitle),
+ actions: [
+ IconButton(
+ key: const Key('countryPickerButton'),
+ icon: const Icon(Icons.public),
+ tooltip: l10n.selectCountryButton,
+ onPressed: () => _openCountryPicker(context),
+ ),
+ ],
+ ),
+ body: SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ if (_showPanicButton(provider))
+ EmergencyButton(
+ key: const Key('panicButton'),
+ emergencyNumber: _panicNumber,
+ ),
+ Expanded(child: _buildContent(context, provider)),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+/// Kurzer Hinweistext für Fehler-/Unknown-/Fallback-Zustände, optional mit
+/// "Erneut versuchen"-Button.
+class _StatusMessage extends StatelessWidget {
+ const _StatusMessage({
+ required this.message,
+ this.retryLabel,
+ this.selectCountryLabel,
+ this.onRetry,
+ this.onSelectCountry,
+ });
+
+ final String message;
+ final String? retryLabel;
+ final String? selectCountryLabel;
+ final VoidCallback? onRetry;
+
+ /// Öffnet den `CountryPickerScreen`, damit der Nutzer sein Land manuell
+ /// auswählen kann, wenn es nicht automatisch ermittelbar war (Plan
+ /// Abschnitt 7, "Land nicht ermittelbar → Panik-Button + Länder-Suchfeld").
+ final VoidCallback? onSelectCountry;
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(message, textAlign: TextAlign.center),
+ if (onRetry != null) ...[
+ const SizedBox(height: 12),
+ ElevatedButton(onPressed: onRetry, child: Text(retryLabel!)),
+ ],
+ if (onSelectCountry != null) ...[
+ const SizedBox(height: 12),
+ TextButton.icon(
+ onPressed: onSelectCountry,
+ icon: const Icon(Icons.public),
+ label: Text(selectCountryLabel!),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+/// Länder-Header + sortierte Liste der landesspezifischen Notruf-Buttons.
+class _CountryContent extends StatelessWidget {
+ const _CountryContent({required this.country});
+
+ final Country country;
+
+ @override
+ Widget build(BuildContext context) {
+ // general zuoberst, danach der Rest in JSON-Reihenfolge.
+ final numbers = [
+ ...country.numbers.where((n) => n.type == EmergencyType.general),
+ ...country.numbers.where((n) => n.type != EmergencyType.general),
+ ];
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ CountryHeader(country: country),
+ Expanded(
+ child: ListView.builder(
+ itemCount: numbers.length,
+ itemBuilder: (context, index) {
+ final number = numbers[index];
+ return EmergencyButton(
+ key: ValueKey(number.type),
+ emergencyNumber: number,
+ );
+ },
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/features/home/widgets/country_header.dart b/lib/features/home/widgets/country_header.dart
new file mode 100644
index 0000000..abd723c
--- /dev/null
+++ b/lib/features/home/widgets/country_header.dart
@@ -0,0 +1,35 @@
+import 'package:flutter/material.dart';
+
+import '../../../models/country.dart';
+
+/// Kompakter Header, der Flagge und Name eines [Country] anzeigt.
+class CountryHeader extends StatelessWidget {
+ const CountryHeader({super.key, required this.country});
+
+ final Country country;
+
+ /// Wählt `nameDe`/`nameEn` (AP1-Datenfeld, keine ARB-Übersetzung – die
+ /// Notrufnummern-Datenbasis kennt bewusst nur diese zwei Sprachen, siehe
+ /// `docs/plan.md` Abschnitt 4) passend zur aktuellen App-Locale aus.
+ String _localizedName(BuildContext context) {
+ final languageCode = Localizations.localeOf(context).languageCode;
+ return languageCode == 'de' ? country.nameDe : country.nameEn;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ child: Row(
+ children: [
+ Text(country.flag, style: const TextStyle(fontSize: 32)),
+ const SizedBox(width: 12),
+ Text(
+ _localizedName(context),
+ style: Theme.of(context).textTheme.titleLarge,
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/features/home/widgets/emergency_button.dart b/lib/features/home/widgets/emergency_button.dart
new file mode 100644
index 0000000..7c930cd
--- /dev/null
+++ b/lib/features/home/widgets/emergency_button.dart
@@ -0,0 +1,110 @@
+import 'package:flutter/material.dart';
+
+import '../../../l10n/app_localizations.dart';
+import '../../../models/emergency_number.dart';
+import '../../../models/emergency_type.dart';
+import '../../../services/dialer_service.dart';
+
+/// Großer, gut antippbarer Button für eine einzelne [EmergencyNumber].
+///
+/// Löst den Anruf direkt über [DialerService] aus – dafür ist kein
+/// Callback nach oben nötig, da der Button keine weitere UI-Logik
+/// beeinflusst. Kann der Wähler nicht geöffnet werden, zeigt der Button
+/// die Nummer als einfachen Fallback in einer [SnackBar] an.
+class EmergencyButton extends StatelessWidget {
+ const EmergencyButton({
+ super.key,
+ required this.emergencyNumber,
+ this.dialerService = const DialerService(),
+ });
+
+ final EmergencyNumber emergencyNumber;
+ final DialerService dialerService;
+
+ IconData get _icon {
+ switch (emergencyNumber.type) {
+ case EmergencyType.general:
+ return Icons.emergency;
+ case EmergencyType.police:
+ return Icons.local_police;
+ case EmergencyType.fire:
+ return Icons.local_fire_department;
+ case EmergencyType.ambulance:
+ return Icons.local_hospital;
+ case EmergencyType.seaRescue:
+ return Icons.directions_boat;
+ case EmergencyType.mountainRescue:
+ return Icons.terrain;
+ }
+ }
+
+ Future _onTap(BuildContext context) async {
+ final success = await dialerService.call(emergencyNumber.number);
+ if (!success && context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(emergencyNumber.number)),
+ );
+ }
+ }
+
+ /// Wählt `labelDe`/`labelEn` (AP1-Datenfeld, keine ARB-Übersetzung – die
+ /// Notrufnummern-Datenbasis kennt bewusst nur diese zwei Sprachen, siehe
+ /// `docs/plan.md` Abschnitt 4) passend zur aktuellen App-Locale aus.
+ String _localizedNumberLabel(BuildContext context) {
+ final languageCode = Localizations.localeOf(context).languageCode;
+ return languageCode == 'de' ? emergencyNumber.labelDe : emergencyNumber.labelEn;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final label = _localizedNumberLabel(context);
+ final semanticLabel = AppLocalizations.of(
+ context,
+ )!.emergencyButtonSemanticLabel(label, emergencyNumber.number);
+
+ return Semantics(
+ button: true,
+ label: semanticLabel,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ child: SizedBox(
+ width: double.infinity,
+ height: 96,
+ child: ElevatedButton(
+ onPressed: () => _onTap(context),
+ style: ElevatedButton.styleFrom(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16),
+ ),
+ ),
+ child: Tooltip(
+ message: semanticLabel,
+ child: Row(
+ children: [
+ Icon(_icon, size: 40),
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ label,
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ Text(
+ emergencyNumber.number,
+ style: Theme.of(context).textTheme.headlineMedium,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
new file mode 100644
index 0000000..aa2540c
--- /dev/null
+++ b/lib/l10n/app_de.arb
@@ -0,0 +1,13 @@
+{
+ "@@locale": "de",
+ "appTitle": "Notruf",
+ "selectCountryButton": "Land manuell wählen",
+ "retryButton": "Erneut versuchen",
+ "errorGeneric": "Unbekannter Fehler beim Laden der Notrufnummern.",
+ "countryUnknownMessage": "Land konnte nicht ermittelt werden.",
+ "countryNotInDatabaseMessage": "Land nicht in der Notrufnummern-Datenbasis hinterlegt. Es steht nur der globale Notruf zur Verfügung.",
+ "pickerTitle": "Land auswählen",
+ "pickerSearchHint": "Land suchen",
+ "pickerNoResults": "Kein Land gefunden.",
+ "emergencyButtonSemanticLabel": "{label}, {number} anrufen"
+}
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 7dc6d05..e86a47c 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -1,3 +1,51 @@
{
- "@@locale": "en"
+ "@@locale": "en",
+ "appTitle": "Emergency",
+ "@appTitle": {
+ "description": "AppBar title of the home screen"
+ },
+ "selectCountryButton": "Select country manually",
+ "@selectCountryButton": {
+ "description": "Label of the button/tooltip used to open the manual country picker"
+ },
+ "retryButton": "Try again",
+ "@retryButton": {
+ "description": "Label of the button that retries country/number detection after an error"
+ },
+ "errorGeneric": "Unknown error while loading the emergency numbers.",
+ "@errorGeneric": {
+ "description": "Fallback error message shown when the provider does not report a specific error"
+ },
+ "countryUnknownMessage": "Country could not be determined.",
+ "@countryUnknownMessage": {
+ "description": "Shown when the country could not be detected automatically (and no location permission info is shown separately)"
+ },
+ "countryNotInDatabaseMessage": "Country not listed in the emergency number database. Only the global emergency number is available.",
+ "@countryNotInDatabaseMessage": {
+ "description": "Shown when the detected country has no entry in the emergency number database"
+ },
+ "pickerTitle": "Select country",
+ "@pickerTitle": {
+ "description": "AppBar title of the manual country picker screen"
+ },
+ "pickerSearchHint": "Search country",
+ "@pickerSearchHint": {
+ "description": "Hint text of the search field in the manual country picker screen"
+ },
+ "pickerNoResults": "No country found.",
+ "@pickerNoResults": {
+ "description": "Empty state shown when the search filter matches no country"
+ },
+ "emergencyButtonSemanticLabel": "{label}, call {number}",
+ "@emergencyButtonSemanticLabel": {
+ "description": "Semantics/tooltip label of an emergency button, combining the number's label and the number itself",
+ "placeholders": {
+ "label": {
+ "type": "String"
+ },
+ "number": {
+ "type": "String"
+ }
+ }
+ }
}
diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb
new file mode 100644
index 0000000..f41a04b
--- /dev/null
+++ b/lib/l10n/app_es.arb
@@ -0,0 +1,13 @@
+{
+ "@@locale": "es",
+ "appTitle": "Emergencia",
+ "selectCountryButton": "Elegir país manualmente",
+ "retryButton": "Reintentar",
+ "errorGeneric": "Error desconocido al cargar los números de emergencia.",
+ "countryUnknownMessage": "No se pudo determinar el país.",
+ "countryNotInDatabaseMessage": "País no disponible en la base de datos de números de emergencia. Solo está disponible el número de emergencia global.",
+ "pickerTitle": "Seleccionar país",
+ "pickerSearchHint": "Buscar país",
+ "pickerNoResults": "No se encontró ningún país.",
+ "emergencyButtonSemanticLabel": "{label}, llamar al {number}"
+}
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
new file mode 100644
index 0000000..17595c2
--- /dev/null
+++ b/lib/l10n/app_fr.arb
@@ -0,0 +1,13 @@
+{
+ "@@locale": "fr",
+ "appTitle": "Urgence",
+ "selectCountryButton": "Choisir le pays manuellement",
+ "retryButton": "Réessayer",
+ "errorGeneric": "Erreur inconnue lors du chargement des numéros d'urgence.",
+ "countryUnknownMessage": "Impossible de déterminer le pays.",
+ "countryNotInDatabaseMessage": "Pays non répertorié dans la base de données des numéros d'urgence. Seul le numéro d'urgence global est disponible.",
+ "pickerTitle": "Sélectionner un pays",
+ "pickerSearchHint": "Rechercher un pays",
+ "pickerNoResults": "Aucun pays trouvé.",
+ "emergencyButtonSemanticLabel": "{label}, appeler le {number}"
+}
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index 0f3a0fa..88b7197 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -5,7 +5,10 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
+import 'app_localizations_de.dart';
import 'app_localizations_en.dart';
+import 'app_localizations_es.dart';
+import 'app_localizations_fr.dart';
// ignore_for_file: type=lint
@@ -92,7 +95,72 @@ abstract class AppLocalizations {
];
/// A list of this localizations delegate's supported locales.
- static const List supportedLocales = [Locale('en')];
+ static const List supportedLocales = [
+ Locale('de'),
+ Locale('en'),
+ Locale('es'),
+ Locale('fr'),
+ ];
+
+ /// AppBar title of the home screen
+ ///
+ /// In en, this message translates to:
+ /// **'Emergency'**
+ String get appTitle;
+
+ /// Label of the button/tooltip used to open the manual country picker
+ ///
+ /// In en, this message translates to:
+ /// **'Select country manually'**
+ String get selectCountryButton;
+
+ /// Label of the button that retries country/number detection after an error
+ ///
+ /// In en, this message translates to:
+ /// **'Try again'**
+ String get retryButton;
+
+ /// Fallback error message shown when the provider does not report a specific error
+ ///
+ /// In en, this message translates to:
+ /// **'Unknown error while loading the emergency numbers.'**
+ String get errorGeneric;
+
+ /// Shown when the country could not be detected automatically (and no location permission info is shown separately)
+ ///
+ /// In en, this message translates to:
+ /// **'Country could not be determined.'**
+ String get countryUnknownMessage;
+
+ /// Shown when the detected country has no entry in the emergency number database
+ ///
+ /// In en, this message translates to:
+ /// **'Country not listed in the emergency number database. Only the global emergency number is available.'**
+ String get countryNotInDatabaseMessage;
+
+ /// AppBar title of the manual country picker screen
+ ///
+ /// In en, this message translates to:
+ /// **'Select country'**
+ String get pickerTitle;
+
+ /// Hint text of the search field in the manual country picker screen
+ ///
+ /// In en, this message translates to:
+ /// **'Search country'**
+ String get pickerSearchHint;
+
+ /// Empty state shown when the search filter matches no country
+ ///
+ /// In en, this message translates to:
+ /// **'No country found.'**
+ String get pickerNoResults;
+
+ /// Semantics/tooltip label of an emergency button, combining the number's label and the number itself
+ ///
+ /// In en, this message translates to:
+ /// **'{label}, call {number}'**
+ String emergencyButtonSemanticLabel(String label, String number);
}
class _AppLocalizationsDelegate
@@ -106,7 +174,7 @@ class _AppLocalizationsDelegate
@override
bool isSupported(Locale locale) =>
- ['en'].contains(locale.languageCode);
+ ['de', 'en', 'es', 'fr'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
@@ -115,8 +183,14 @@ class _AppLocalizationsDelegate
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
+ case 'de':
+ return AppLocalizationsDe();
case 'en':
return AppLocalizationsEn();
+ case 'es':
+ return AppLocalizationsEs();
+ case 'fr':
+ return AppLocalizationsFr();
}
throw FlutterError(
diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart
new file mode 100644
index 0000000..0052343
--- /dev/null
+++ b/lib/l10n/app_localizations_de.dart
@@ -0,0 +1,43 @@
+// ignore: unused_import
+import 'package:intl/intl.dart' as intl;
+import 'app_localizations.dart';
+
+// ignore_for_file: type=lint
+
+/// The translations for German (`de`).
+class AppLocalizationsDe extends AppLocalizations {
+ AppLocalizationsDe([String locale = 'de']) : super(locale);
+
+ @override
+ String get appTitle => 'Notruf';
+
+ @override
+ String get selectCountryButton => 'Land manuell wählen';
+
+ @override
+ String get retryButton => 'Erneut versuchen';
+
+ @override
+ String get errorGeneric => 'Unbekannter Fehler beim Laden der Notrufnummern.';
+
+ @override
+ String get countryUnknownMessage => 'Land konnte nicht ermittelt werden.';
+
+ @override
+ String get countryNotInDatabaseMessage =>
+ 'Land nicht in der Notrufnummern-Datenbasis hinterlegt. Es steht nur der globale Notruf zur Verfügung.';
+
+ @override
+ String get pickerTitle => 'Land auswählen';
+
+ @override
+ String get pickerSearchHint => 'Land suchen';
+
+ @override
+ String get pickerNoResults => 'Kein Land gefunden.';
+
+ @override
+ String emergencyButtonSemanticLabel(String label, String number) {
+ return '$label, $number anrufen';
+ }
+}
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index 5d9907e..a17c0d5 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -7,4 +7,38 @@ import 'app_localizations.dart';
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
+
+ @override
+ String get appTitle => 'Emergency';
+
+ @override
+ String get selectCountryButton => 'Select country manually';
+
+ @override
+ String get retryButton => 'Try again';
+
+ @override
+ String get errorGeneric =>
+ 'Unknown error while loading the emergency numbers.';
+
+ @override
+ String get countryUnknownMessage => 'Country could not be determined.';
+
+ @override
+ String get countryNotInDatabaseMessage =>
+ 'Country not listed in the emergency number database. Only the global emergency number is available.';
+
+ @override
+ String get pickerTitle => 'Select country';
+
+ @override
+ String get pickerSearchHint => 'Search country';
+
+ @override
+ String get pickerNoResults => 'No country found.';
+
+ @override
+ String emergencyButtonSemanticLabel(String label, String number) {
+ return '$label, call $number';
+ }
}
diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart
new file mode 100644
index 0000000..5e46194
--- /dev/null
+++ b/lib/l10n/app_localizations_es.dart
@@ -0,0 +1,44 @@
+// ignore: unused_import
+import 'package:intl/intl.dart' as intl;
+import 'app_localizations.dart';
+
+// ignore_for_file: type=lint
+
+/// The translations for Spanish Castilian (`es`).
+class AppLocalizationsEs extends AppLocalizations {
+ AppLocalizationsEs([String locale = 'es']) : super(locale);
+
+ @override
+ String get appTitle => 'Emergencia';
+
+ @override
+ String get selectCountryButton => 'Elegir país manualmente';
+
+ @override
+ String get retryButton => 'Reintentar';
+
+ @override
+ String get errorGeneric =>
+ 'Error desconocido al cargar los números de emergencia.';
+
+ @override
+ String get countryUnknownMessage => 'No se pudo determinar el país.';
+
+ @override
+ String get countryNotInDatabaseMessage =>
+ 'País no disponible en la base de datos de números de emergencia. Solo está disponible el número de emergencia global.';
+
+ @override
+ String get pickerTitle => 'Seleccionar país';
+
+ @override
+ String get pickerSearchHint => 'Buscar país';
+
+ @override
+ String get pickerNoResults => 'No se encontró ningún país.';
+
+ @override
+ String emergencyButtonSemanticLabel(String label, String number) {
+ return '$label, llamar al $number';
+ }
+}
diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart
new file mode 100644
index 0000000..5089e99
--- /dev/null
+++ b/lib/l10n/app_localizations_fr.dart
@@ -0,0 +1,44 @@
+// ignore: unused_import
+import 'package:intl/intl.dart' as intl;
+import 'app_localizations.dart';
+
+// ignore_for_file: type=lint
+
+/// The translations for French (`fr`).
+class AppLocalizationsFr extends AppLocalizations {
+ AppLocalizationsFr([String locale = 'fr']) : super(locale);
+
+ @override
+ String get appTitle => 'Urgence';
+
+ @override
+ String get selectCountryButton => 'Choisir le pays manuellement';
+
+ @override
+ String get retryButton => 'Réessayer';
+
+ @override
+ String get errorGeneric =>
+ 'Erreur inconnue lors du chargement des numéros d\'urgence.';
+
+ @override
+ String get countryUnknownMessage => 'Impossible de déterminer le pays.';
+
+ @override
+ String get countryNotInDatabaseMessage =>
+ 'Pays non répertorié dans la base de données des numéros d\'urgence. Seul le numéro d\'urgence global est disponible.';
+
+ @override
+ String get pickerTitle => 'Sélectionner un pays';
+
+ @override
+ String get pickerSearchHint => 'Rechercher un pays';
+
+ @override
+ String get pickerNoResults => 'Aucun pays trouvé.';
+
+ @override
+ String emergencyButtonSemanticLabel(String label, String number) {
+ return '$label, appeler le $number';
+ }
+}
diff --git a/lib/main.dart b/lib/main.dart
index 244a702..d9fd68c 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,122 +1,7 @@
import 'package:flutter/material.dart';
+import 'app.dart';
+
void main() {
- runApp(const MyApp());
-}
-
-class MyApp extends StatelessWidget {
- const MyApp({super.key});
-
- // This widget is the root of your application.
- @override
- Widget build(BuildContext context) {
- return MaterialApp(
- title: 'Flutter Demo',
- theme: ThemeData(
- // This is the theme of your application.
- //
- // TRY THIS: Try running your application with "flutter run". You'll see
- // the application has a purple toolbar. Then, without quitting the app,
- // try changing the seedColor in the colorScheme below to Colors.green
- // and then invoke "hot reload" (save your changes or press the "hot
- // reload" button in a Flutter-supported IDE, or press "r" if you used
- // the command line to start the app).
- //
- // Notice that the counter didn't reset back to zero; the application
- // state is not lost during the reload. To reset the state, use hot
- // restart instead.
- //
- // This works for code too, not just values: Most code changes can be
- // tested with just a hot reload.
- colorScheme: .fromSeed(seedColor: Colors.deepPurple),
- ),
- home: const MyHomePage(title: 'Flutter Demo Home Page'),
- );
- }
-}
-
-class MyHomePage extends StatefulWidget {
- const MyHomePage({super.key, required this.title});
-
- // This widget is the home page of your application. It is stateful, meaning
- // that it has a State object (defined below) that contains fields that affect
- // how it looks.
-
- // This class is the configuration for the state. It holds the values (in this
- // case the title) provided by the parent (in this case the App widget) and
- // used by the build method of the State. Fields in a Widget subclass are
- // always marked "final".
-
- final String title;
-
- @override
- State createState() => _MyHomePageState();
-}
-
-class _MyHomePageState extends State {
- int _counter = 0;
-
- void _incrementCounter() {
- setState(() {
- // This call to setState tells the Flutter framework that something has
- // changed in this State, which causes it to rerun the build method below
- // so that the display can reflect the updated values. If we changed
- // _counter without calling setState(), then the build method would not be
- // called again, and so nothing would appear to happen.
- _counter++;
- });
- }
-
- @override
- Widget build(BuildContext context) {
- // This method is rerun every time setState is called, for instance as done
- // by the _incrementCounter method above.
- //
- // The Flutter framework has been optimized to make rerunning build methods
- // fast, so that you can just rebuild anything that needs updating rather
- // than having to individually change instances of widgets.
- return Scaffold(
- appBar: AppBar(
- // TRY THIS: Try changing the color here to a specific color (to
- // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
- // change color while the other colors stay the same.
- backgroundColor: Theme.of(context).colorScheme.inversePrimary,
- // Here we take the value from the MyHomePage object that was created by
- // the App.build method, and use it to set our appbar title.
- title: Text(widget.title),
- ),
- body: Center(
- // Center is a layout widget. It takes a single child and positions it
- // in the middle of the parent.
- child: Column(
- // Column is also a layout widget. It takes a list of children and
- // arranges them vertically. By default, it sizes itself to fit its
- // children horizontally, and tries to be as tall as its parent.
- //
- // Column has various properties to control how it sizes itself and
- // how it positions its children. Here we use mainAxisAlignment to
- // center the children vertically; the main axis here is the vertical
- // axis because Columns are vertical (the cross axis would be
- // horizontal).
- //
- // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
- // action in the IDE, or press "p" in the console), to see the
- // wireframe for each widget.
- mainAxisAlignment: .center,
- children: [
- const Text('You have pushed the button this many times:'),
- Text(
- '$_counter',
- style: Theme.of(context).textTheme.headlineMedium,
- ),
- ],
- ),
- ),
- floatingActionButton: FloatingActionButton(
- onPressed: _incrementCounter,
- tooltip: 'Increment',
- child: const Icon(Icons.add),
- ),
- );
- }
+ runApp(const App());
}
diff --git a/lib/models/.gitkeep b/lib/models/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/lib/models/country.dart b/lib/models/country.dart
new file mode 100644
index 0000000..9dfe16b
--- /dev/null
+++ b/lib/models/country.dart
@@ -0,0 +1,56 @@
+import 'emergency_number.dart';
+
+/// Ein Land mit seinen landesspezifischen Notrufnummern.
+class Country {
+ const Country({
+ required this.isoCode,
+ required this.nameDe,
+ required this.nameEn,
+ required this.flag,
+ required this.numbers,
+ });
+
+ /// ISO 3166-1 alpha-2 Code, z. B. "DE".
+ final String isoCode;
+ final String nameDe;
+ final String nameEn;
+ final String flag;
+ final List numbers;
+
+ factory Country.fromJson(String isoCode, Map json) {
+ final numbersJson = json['numbers'] as List;
+ return Country(
+ isoCode: isoCode,
+ nameDe: json['name_de'] as String,
+ nameEn: json['name_en'] as String,
+ flag: json['flag'] as String,
+ numbers: numbersJson
+ .map((e) => EmergencyNumber.fromJson(e as Map))
+ .toList(growable: false),
+ );
+ }
+
+ @override
+ String toString() => 'Country(isoCode: $isoCode, nameEn: $nameEn)';
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is Country &&
+ other.isoCode == isoCode &&
+ other.nameDe == nameDe &&
+ other.nameEn == nameEn &&
+ other.flag == flag &&
+ _listEquals(other.numbers, numbers));
+
+ @override
+ int get hashCode => Object.hash(isoCode, nameDe, nameEn, flag, Object.hashAll(numbers));
+
+ static bool _listEquals(List a, List b) {
+ if (a.length != b.length) return false;
+ for (var i = 0; i < a.length; i++) {
+ if (a[i] != b[i]) return false;
+ }
+ return true;
+ }
+}
diff --git a/lib/models/emergency_number.dart b/lib/models/emergency_number.dart
new file mode 100644
index 0000000..1d316df
--- /dev/null
+++ b/lib/models/emergency_number.dart
@@ -0,0 +1,40 @@
+import 'emergency_type.dart';
+
+/// Eine einzelne Notrufnummer eines Landes (z. B. Polizei, Feuerwehr).
+class EmergencyNumber {
+ const EmergencyNumber({
+ required this.type,
+ required this.number,
+ required this.labelDe,
+ required this.labelEn,
+ });
+
+ final EmergencyType type;
+ final String number;
+ final String labelDe;
+ final String labelEn;
+
+ factory EmergencyNumber.fromJson(Map json) {
+ return EmergencyNumber(
+ type: EmergencyType.fromJson(json['type'] as String),
+ number: json['number'] as String,
+ labelDe: json['label_de'] as String,
+ labelEn: json['label_en'] as String,
+ );
+ }
+
+ @override
+ String toString() => 'EmergencyNumber(type: $type, number: $number)';
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is EmergencyNumber &&
+ other.type == type &&
+ other.number == number &&
+ other.labelDe == labelDe &&
+ other.labelEn == labelEn);
+
+ @override
+ int get hashCode => Object.hash(type, number, labelDe, labelEn);
+}
diff --git a/lib/models/emergency_type.dart b/lib/models/emergency_type.dart
new file mode 100644
index 0000000..a626b46
--- /dev/null
+++ b/lib/models/emergency_type.dart
@@ -0,0 +1,50 @@
+/// Kategorie eines Notrufs, bestimmt Icon/Farbe/Priorität im UI.
+enum EmergencyType {
+ general,
+ police,
+ fire,
+ ambulance,
+ seaRescue,
+ mountainRescue;
+
+ /// Wandelt den `type`-String aus `emergency_numbers.json` in den Enum-Wert um.
+ ///
+ /// Wirft eine [FormatException], wenn der Wert unbekannt ist, damit
+ /// fehlerhafte Datensätze beim Laden auffallen statt still ignoriert zu werden.
+ static EmergencyType fromJson(String value) {
+ switch (value) {
+ case 'general':
+ return EmergencyType.general;
+ case 'police':
+ return EmergencyType.police;
+ case 'fire':
+ return EmergencyType.fire;
+ case 'ambulance':
+ return EmergencyType.ambulance;
+ case 'sea_rescue':
+ return EmergencyType.seaRescue;
+ case 'mountain_rescue':
+ return EmergencyType.mountainRescue;
+ default:
+ throw FormatException('Unbekannter EmergencyType: $value');
+ }
+ }
+
+ /// Wandelt den Enum-Wert zurück in den JSON-String (z. B. für Tests/Debugging).
+ String toJson() {
+ switch (this) {
+ case EmergencyType.general:
+ return 'general';
+ case EmergencyType.police:
+ return 'police';
+ case EmergencyType.fire:
+ return 'fire';
+ case EmergencyType.ambulance:
+ return 'ambulance';
+ case EmergencyType.seaRescue:
+ return 'sea_rescue';
+ case EmergencyType.mountainRescue:
+ return 'mountain_rescue';
+ }
+ }
+}
diff --git a/lib/providers/emergency_provider.dart b/lib/providers/emergency_provider.dart
new file mode 100644
index 0000000..f39e56f
--- /dev/null
+++ b/lib/providers/emergency_provider.dart
@@ -0,0 +1,125 @@
+import 'package:flutter/foundation.dart';
+
+import '../models/country.dart';
+import '../repositories/emergency_number_repository.dart';
+import '../services/location_service.dart';
+import 'emergency_status.dart';
+
+/// Orchestriert Repository + Ländererkennung für die UI
+/// (`docs/plan.md`, Abschnitt 5 + 6 + 8, AP5).
+///
+/// Kennt keinen `BuildContext`; die UI (`HomeScreen`) beobachtet [status]/
+/// [country]/[errorMessage] über `ChangeNotifier` und ruft [init]/[retry] auf.
+///
+/// **Entscheidung zu `EmergencyStatus.permissionDenied`:** `LocationService`
+/// liefert bewusst nur `String? isoCode` ohne Fehlergrund (AP4-Entscheidung,
+/// um die Ermittlungskette einfach zu halten). Eine zusätzliche, direkte
+/// Abfrage z. B. von `Geolocator.checkPermission()` hier im Provider würde
+/// eine plattformnahe Geolocator-Abhängigkeit in die State-Schicht ziehen,
+/// obwohl das reine Wissen um "GPS-Berechtigung verweigert" den eigentlichen
+/// Ermittlungs-Flow gar nicht beeinflusst (SIM/Cache können trotzdem einen
+/// Treffer liefern → dann ist der Status weiterhin `ready`, nicht
+/// `permissionDenied`). Das würde Zuständigkeiten vermischen, ohne den
+/// Nutzer:innen im Ladezustand einen Mehrwert zu bieten. Deshalb bleibt
+/// `permissionDenied` für AP5 unbenutzt; der Fall "GPS verweigert und keine
+/// andere Quelle liefert einen Code" wird korrekt (und ausreichend) über
+/// `countryUnknown` abgebildet. Eine gezieltere Unterscheidung kann bei
+/// Bedarf in einem späteren AP (Banner/UX-Feinschliff) ergänzt werden.
+class EmergencyProvider extends ChangeNotifier {
+ // Bewusst keine initializing formals (`this._repository`): Die externen
+ // Konstruktorparameter sollen lesbar `repository`/`locationService`
+ // heißen, die Felder aber `_repository`/`_locationService` (privat).
+ EmergencyProvider({required EmergencyNumberRepository repository, required LocationService locationService})
+ : _repository = repository, // ignore: prefer_initializing_formals
+ _locationService = locationService; // ignore: prefer_initializing_formals
+
+ final EmergencyNumberRepository _repository;
+ final LocationService _locationService;
+
+ EmergencyStatus _status = EmergencyStatus.initializing;
+ Country? _country;
+ String? _errorMessage;
+ bool _disposed = false;
+
+ EmergencyStatus get status => _status;
+ Country? get country => _country;
+ String? get errorMessage => _errorMessage;
+
+ /// Exponiert das Repository für die UI (`docs/plan.md`, Abschnitt 8, AP6):
+ /// Der `CountryPickerScreen` braucht Zugriff auf die bereits geladene
+ /// Länderliste (`EmergencyNumberRepository.allCountries`), ohne eine
+ /// zweite, unabhängige Repository-Instanz zu erzeugen oder das Asset
+ /// erneut zu laden (`load()` ist idempotent, aber unnötig).
+ EmergencyNumberRepository get repository => _repository;
+
+ @override
+ void dispose() {
+ _disposed = true;
+ super.dispose();
+ }
+
+ /// Wie [notifyListeners], aber ein no-op, wenn der Provider inzwischen
+ /// disposed wurde – kann passieren, wenn [init]/[retry] noch auf eine
+ /// echte Asset-/Plugin-I/O wartet (z. B. asset load, GPS), während die
+ /// UI (z. B. `HomeScreen`) längst nicht mehr im Widgetbaum ist.
+ void _notifyIfActive() {
+ if (!_disposed) notifyListeners();
+ }
+
+ /// Startet die Ermittlungskette: Datenbasis laden → Ländercode ermitteln
+ /// → Land nachschlagen. Wirft nie; Fehler landen im `error`-Status.
+ Future init() async {
+ _status = EmergencyStatus.initializing;
+ _country = null;
+ _errorMessage = null;
+ _notifyIfActive();
+
+ try {
+ await _repository.load();
+ } catch (e) {
+ _status = EmergencyStatus.error;
+ _errorMessage = e.toString();
+ _notifyIfActive();
+ return;
+ }
+
+ _status = EmergencyStatus.locating;
+ _notifyIfActive();
+
+ final isoCode = await _locationService.resolveCountryCode();
+ if (isoCode == null) {
+ _status = EmergencyStatus.countryUnknown;
+ _country = null;
+ _notifyIfActive();
+ return;
+ }
+
+ // country bleibt `null`, wenn der ermittelte isoCode nicht in der
+ // Datenbasis vorhanden ist (Plan Abschnitt 7) – Status ist dennoch
+ // `ready`, die UI fällt dann auf den globalen Panik-Button zurück.
+ _country = _repository.lookup(isoCode);
+ _status = EmergencyStatus.ready;
+ _notifyIfActive();
+ }
+
+ /// Wiederholt die komplette Ermittlungskette (kein separater Zustand nötig).
+ Future retry() => init();
+
+ /// Setzt die manuelle Länderauswahl des Nutzers (`docs/plan.md`,
+ /// Abschnitt 8, AP6: `CountryPickerScreen`) und stößt die
+ /// Ermittlungskette danach sofort erneut an, damit [isoCode] unmittelbar
+ /// wirksam wird (die manuelle Auswahl hat in [LocationService] höchste
+ /// Priorität vor SIM/GPS/Cache).
+ Future selectCountryManually(String isoCode) async {
+ await _locationService.setManualCountryCode(isoCode);
+ await init();
+ }
+
+ /// Löscht die manuelle Länderauswahl wieder und ermittelt das Land danach
+ /// erneut automatisch (SIM → GPS → Cache) – erlaubt dem Nutzer, zur
+ /// automatischen Erkennung zurückzukehren (`docs/plan.md`, Abschnitt 8, AP6).
+ Future clearManualSelection() async {
+ await _locationService.setManualCountryCode(null);
+ await init();
+ }
+}
diff --git a/lib/providers/emergency_status.dart b/lib/providers/emergency_status.dart
new file mode 100644
index 0000000..43312e1
--- /dev/null
+++ b/lib/providers/emergency_status.dart
@@ -0,0 +1,36 @@
+/// Zustände der Ländererkennungs-/Notrufnummern-Ermittlung
+/// (`docs/plan.md`, Abschnitt 5 + 8, AP5).
+///
+/// Siehe [EmergencyProvider] für die Zustandsübergänge.
+enum EmergencyStatus {
+ /// [EmergencyProvider.init] wurde gerade gestartet, die Notrufnummern-
+ /// Datenbasis wird geladen.
+ initializing,
+
+ /// Die Datenbasis ist geladen, die Ländererkennungskette
+ /// (SIM → GPS → Cache) läuft gerade.
+ locating,
+
+ /// Ermittlung abgeschlossen. `country` ist gesetzt, wenn das ermittelte
+ /// Land in der Datenbasis vorhanden ist; ist es `null`, wurde zwar ein
+ /// Ländercode ermittelt, aber kein Eintrag dafür gefunden (Plan
+ /// Abschnitt 7, "Land nicht in Datenbasis") – die UI zeigt dann nur den
+ /// globalen Panik-Button.
+ ready,
+
+ /// Aktuell unbenutzt (siehe Doc-Kommentar in [EmergencyProvider]): Die
+ /// Standortberechtigung wurde verweigert. Dieser Fall wird zur Laufzeit
+ /// bewusst nicht von [countryUnknown] unterschieden, ist als Status aber
+ /// für eine spätere, gezieltere UI-Behandlung (z. B. Banner mit
+ /// Berechtigungs-Hinweis) bereits vorbereitet.
+ permissionDenied,
+
+ /// Kein Ländercode ermittelbar (alle Quellen der Ermittlungskette lieferten
+ /// `null`, Plan Abschnitt 7, "Land nicht ermittelbar").
+ countryUnknown,
+
+ /// Laden der Notrufnummern-Datenbasis ist fehlgeschlagen (z. B. kaputtes
+ /// JSON-Asset). Sollte im Normalbetrieb nicht auftreten, siehe Doc-Kommentar
+ /// in `EmergencyNumberRepository.load()`.
+ error,
+}
diff --git a/lib/repositories/.gitkeep b/lib/repositories/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/lib/repositories/emergency_number_repository.dart b/lib/repositories/emergency_number_repository.dart
new file mode 100644
index 0000000..2dc8acd
--- /dev/null
+++ b/lib/repositories/emergency_number_repository.dart
@@ -0,0 +1,73 @@
+import 'dart:convert';
+
+import 'package:flutter/services.dart' show rootBundle;
+
+import '../models/country.dart';
+
+/// Lädt und cacht die statische Notrufnummern-Datenbasis aus
+/// `assets/data/emergency_numbers.json`.
+///
+/// Die Datei wird nur beim ersten [load]-Aufruf eingelesen und geparst;
+/// danach liefert [lookup] aus dem In-Memory-Cache.
+class EmergencyNumberRepository {
+ EmergencyNumberRepository({this.assetPath = 'assets/data/emergency_numbers.json'});
+
+ final String assetPath;
+
+ Map? _countriesByIsoCode;
+
+ /// True, sobald [load] erfolgreich durchgelaufen ist.
+ bool get isLoaded => _countriesByIsoCode != null;
+
+ /// Liest und parst die JSON-Datei einmalig ein.
+ ///
+ /// Wird [load] mehrfach aufgerufen, geschieht nach dem ersten
+ /// erfolgreichen Durchlauf nichts mehr (Cache bleibt bestehen).
+ ///
+ /// Wirft eine [FormatException], wenn das JSON fehlerhaft ist – das ist
+ /// ein Programmierfehler (kaputtes Asset) und soll beim Start auffallen,
+ /// nicht still verschluckt werden.
+ Future load() async {
+ if (isLoaded) return;
+
+ final raw = await rootBundle.loadString(assetPath);
+ final decoded = jsonDecode(raw);
+
+ if (decoded is! Map) {
+ throw const FormatException(
+ 'emergency_numbers.json: erwartetes Top-Level-Objekt fehlt',
+ );
+ }
+
+ final parsed = {};
+ for (final entry in decoded.entries) {
+ final isoCode = entry.key.toUpperCase();
+ parsed[isoCode] = Country.fromJson(isoCode, entry.value as Map);
+ }
+ _countriesByIsoCode = parsed;
+ }
+
+ /// Sucht ein Land anhand seines ISO 3166-1 alpha-2 Codes (case-insensitive).
+ ///
+ /// Gibt `null` zurück, wenn [load] noch nicht aufgerufen wurde oder das
+ /// Land nicht in der Datenbasis vorhanden ist – bewusst keine Exception,
+ /// damit Aufrufer den Fallback-Pfad (z. B. globaler 112/911-Button) ohne
+ /// try/catch abbilden können.
+ Country? lookup(String isoCode) {
+ final countries = _countriesByIsoCode;
+ if (countries == null) return null;
+ return countries[isoCode.toUpperCase()];
+ }
+
+ /// Alle Länder der Datenbasis, alphabetisch nach [Country.nameDe] sortiert
+ /// (`docs/plan.md`, Abschnitt 8, AP6: Grundlage für den `CountryPickerScreen`).
+ ///
+ /// Liefert eine leere Liste, wenn [load] noch nicht (erfolgreich)
+ /// durchgelaufen ist – bewusst kein Fehler, analog zu [lookup].
+ List get allCountries {
+ final countries = _countriesByIsoCode;
+ if (countries == null) return const [];
+ final sorted = countries.values.toList()..sort((a, b) => a.nameDe.compareTo(b.nameDe));
+ return sorted;
+ }
+}
diff --git a/lib/services/country_code_sources/.gitkeep b/lib/services/country_code_sources/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/lib/services/country_code_sources/cached_country_code_source.dart b/lib/services/country_code_sources/cached_country_code_source.dart
new file mode 100644
index 0000000..abf19fb
--- /dev/null
+++ b/lib/services/country_code_sources/cached_country_code_source.dart
@@ -0,0 +1,40 @@
+import 'package:shared_preferences/shared_preferences.dart';
+
+import 'country_code_source.dart';
+
+/// Liest/schreibt den zuletzt erfolgreich ermittelten ISO-Ländercode aus
+/// `shared_preferences` (`docs/plan.md`, Abschnitt 6, letzter Fallback vor
+/// `countryUnknown`).
+class CachedCountryCodeSource implements CountryCodeCache {
+ const CachedCountryCodeSource();
+
+ static const _key = 'last_known_country_code';
+
+ @override
+ Future resolve() async {
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ final code = prefs.getString(_key)?.trim();
+ if (code == null || code.isEmpty) return null;
+ return code.toUpperCase();
+ } catch (_) {
+ return null;
+ }
+ }
+
+ /// Speichert [isoCode] als zuletzt bekannten Ländercode.
+ ///
+ /// Wird von [LocationService] nach jeder erfolgreichen Ermittlung über
+ /// SIM oder GPS aufgerufen. Best effort: Schreibfehler werden verschluckt,
+ /// damit ein kaputter Cache die eigentliche Ländererkennung nicht
+ /// beeinflusst.
+ @override
+ Future save(String isoCode) async {
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setString(_key, isoCode.toUpperCase());
+ } catch (_) {
+ // Best effort, siehe Doc-Kommentar oben.
+ }
+ }
+}
diff --git a/lib/services/country_code_sources/country_code_source.dart b/lib/services/country_code_sources/country_code_source.dart
new file mode 100644
index 0000000..0bf026a
--- /dev/null
+++ b/lib/services/country_code_sources/country_code_source.dart
@@ -0,0 +1,35 @@
+/// Gemeinsame Schnittstelle für Ländercode-Ermittlungsquellen
+/// (`docs/plan.md`, Abschnitt 6 + 8, AP4).
+///
+/// Implementierungen liefern best-effort einen ISO 3166-1 alpha-2
+/// Ländercode oder `null`, wenn sie ihn nicht ermitteln konnten. Sie dürfen
+/// dabei **niemals** werfen – Fehler (fehlende Berechtigung, kein Netz,
+/// Plugin-Fehler auf Emulatoren o. Ä.) werden intern abgefangen und als
+/// `null` codiert. Das erlaubt [LocationService], die Kette gleichförmig zu
+/// orchestrieren, ohne pro Quelle try/catch zu benötigen.
+abstract class CountryCodeSource {
+ Future resolve();
+}
+
+/// Erweiterung von [CountryCodeSource] um eine Schreibmöglichkeit.
+///
+/// Wird von der Cache-Quelle implementiert: Sie liefert nicht nur den
+/// zuletzt bekannten Ländercode zurück ([resolve]), sondern erlaubt auch,
+/// einen neu ermittelten Code zu speichern ([save]) – siehe
+/// `CachedCountryCodeSource`.
+abstract class CountryCodeCache implements CountryCodeSource {
+ /// Speichert [isoCode] als zuletzt bekannten Ländercode (best effort).
+ Future save(String isoCode);
+}
+
+/// Erweiterung von [CountryCodeSource] um die manuelle Länderauswahl
+/// (`docs/plan.md`, Abschnitt 8, AP6).
+///
+/// Im Gegensatz zu [CountryCodeCache] erlaubt [save] hier explizit `null`,
+/// damit der Nutzer die manuelle Auswahl wieder löschen und zur
+/// automatischen Ermittlungskette (SIM → GPS → Cache) zurückkehren kann.
+abstract class ManualCountryCodeStore implements CountryCodeSource {
+ /// Setzt die manuelle Länderauswahl. `null` löscht eine zuvor gesetzte
+ /// Auswahl wieder (best effort).
+ Future save(String? isoCode);
+}
diff --git a/lib/services/country_code_sources/gps_country_code_source.dart b/lib/services/country_code_sources/gps_country_code_source.dart
new file mode 100644
index 0000000..8172d09
--- /dev/null
+++ b/lib/services/country_code_sources/gps_country_code_source.dart
@@ -0,0 +1,64 @@
+import 'package:geocoding/geocoding.dart';
+import 'package:geolocator/geolocator.dart';
+
+import 'country_code_source.dart';
+
+/// Ermittelt den ISO-Ländercode per GPS-Position + Reverse-Geocoding
+/// (`docs/plan.md`, Abschnitt 6, Fallback-Quelle Nr. 2 – insbesondere auf
+/// iOS 16+ wichtig, da die SIM-Quelle dort eingeschränkt ist, siehe
+/// Abschnitt 9).
+///
+/// Nutzt bewusst nur die in `geolocator` eingebaute Berechtigungs-API
+/// (`checkPermission()` / `requestPermission()`) – ein zusätzlicher Einsatz
+/// von `permission_handler` wäre redundant (AP4-Vorgabe).
+///
+/// Liefert `null`, sobald irgendein Schritt scheitert: verweigerte/dauerhaft
+/// verweigerte Berechtigung, deaktivierte Standortdienste, kein GPS-Fix
+/// innerhalb des Zeitlimits, oder ein Netzwerkfehler beim Reverse-Geocoding
+/// (`placemarkFromCoordinates` braucht ggf. Netz). Wirft dabei nie eine
+/// Exception – das ist im Plan als Edge Case vorgesehen (Abschnitt 7): Die
+/// Kette fällt dann weiter auf den Cache zurück.
+class GpsCountryCodeSource implements CountryCodeSource {
+ GpsCountryCodeSource({Geocoding? geocoding}) : _geocoding = geocoding ?? Geocoding();
+
+ final Geocoding _geocoding;
+
+ static const _timeout = Duration(seconds: 10);
+
+ @override
+ Future resolve() async {
+ try {
+ if (!await _hasPermission()) return null;
+
+ final position = await Geolocator.getCurrentPosition(
+ locationSettings: const LocationSettings(
+ accuracy: LocationAccuracy.low,
+ timeLimit: _timeout,
+ ),
+ );
+
+ final placemarks = await _geocoding.placemarkFromCoordinates(
+ position.latitude,
+ position.longitude,
+ );
+
+ for (final placemark in placemarks) {
+ final code = placemark.isoCountryCode?.trim();
+ if (code != null && code.isNotEmpty) {
+ return code.toUpperCase();
+ }
+ }
+ return null;
+ } catch (_) {
+ return null;
+ }
+ }
+
+ Future _hasPermission() async {
+ var permission = await Geolocator.checkPermission();
+ if (permission == LocationPermission.denied) {
+ permission = await Geolocator.requestPermission();
+ }
+ return permission == LocationPermission.whileInUse || permission == LocationPermission.always;
+ }
+}
diff --git a/lib/services/country_code_sources/manual_country_code_source.dart b/lib/services/country_code_sources/manual_country_code_source.dart
new file mode 100644
index 0000000..912f070
--- /dev/null
+++ b/lib/services/country_code_sources/manual_country_code_source.dart
@@ -0,0 +1,46 @@
+import 'package:shared_preferences/shared_preferences.dart';
+
+import 'country_code_source.dart';
+
+/// Liest/schreibt den vom Nutzer manuell gewählten ISO-Ländercode aus
+/// `shared_preferences` (`docs/plan.md`, Abschnitt 8, AP6).
+///
+/// Bewusst getrennt von [CachedCountryCodeSource] (eigener Key, eigene
+/// Klasse): Die manuelle Auswahl ist ein Nutzer-Override und darf vom
+/// automatisch zuletzt ermittelten Code (SIM/GPS-Treffer) nicht überschrieben
+/// oder mit ihm verwechselt werden. `LocationService` fragt diese Quelle
+/// deshalb mit höchster Priorität vor SIM/GPS/Cache ab.
+class ManualCountryCodeSource implements ManualCountryCodeStore {
+ const ManualCountryCodeSource();
+
+ static const _key = 'manual_country_code';
+
+ @override
+ Future resolve() async {
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ final code = prefs.getString(_key)?.trim();
+ if (code == null || code.isEmpty) return null;
+ return code.toUpperCase();
+ } catch (_) {
+ return null;
+ }
+ }
+
+ /// Setzt die manuelle Länderauswahl. `isoCode == null` löscht eine zuvor
+ /// gesetzte Auswahl wieder, damit der Nutzer zur automatischen Ermittlung
+ /// zurückkehren kann. Best effort: Schreibfehler werden verschluckt.
+ @override
+ Future save(String? isoCode) async {
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ if (isoCode == null) {
+ await prefs.remove(_key);
+ } else {
+ await prefs.setString(_key, isoCode.toUpperCase());
+ }
+ } catch (_) {
+ // Best effort, siehe Doc-Kommentar oben.
+ }
+ }
+}
diff --git a/lib/services/country_code_sources/sim_country_code_source.dart b/lib/services/country_code_sources/sim_country_code_source.dart
new file mode 100644
index 0000000..4b2904a
--- /dev/null
+++ b/lib/services/country_code_sources/sim_country_code_source.dart
@@ -0,0 +1,85 @@
+import 'package:carrier_info/carrier_info.dart';
+import 'package:flutter/foundation.dart';
+
+import 'country_code_source.dart';
+
+/// Ermittelt den ISO-Ländercode aus SIM-/Netzwerk-Informationen via
+/// `carrier_info` (`docs/plan.md`, Abschnitt 2 + 3: primäre, offline
+/// verfügbare Quelle).
+///
+/// **Android:** Priorität liegt auf `networkCountryIso` des aktuell
+/// eingebuchten Mobilfunknetzes (aus `telephonyInfo`) – nicht auf dem
+/// Heimatland der SIM. Genau dieses Netz wickelt einen Notruf ab, ändert
+/// sich also korrekt bei Roaming (siehe Plan, Zeile "Bei Konflikt ...
+/// gewinnt automatisch die SIM-Quelle"). Fällt zurück auf
+/// `isoCountryCode` (SIM-Ausgabeland laut Telephony API) und zuletzt auf
+/// `countryIso` aus `subscriptionsInfo`.
+///
+/// **iOS:** Apple hat `CTCarrier` ab iOS 16 deprecated (`docs/plan.md`,
+/// Abschnitt 9 – verbleibendes Risiko). `carrier_info` liefert auf iOS
+/// 16+ für `isoCountryCode` verlässlich `null` zurück (siehe Paket-Doku
+/// `iOS_16_DEPRECATION_NOTICE.md`). Das ist eine bekannte Plattform-
+/// Einschränkung, kein Bug: Diese Quelle liefert dann einfach `null`,
+/// wodurch `LocationService` automatisch auf GPS/Reverse-Geocoding
+/// zurückfällt.
+///
+/// Alle Plugin-Aufrufe sind defensiv in try/catch gekapselt: Auf
+/// Emulatoren, Geräten ohne SIM oder bei Plugin-Fehlern kann
+/// `carrier_info` werfen oder leere/`null`-Werte liefern – in jedem Fall
+/// liefert [resolve] dann `null`, nie eine Exception.
+class SimCountryCodeSource implements CountryCodeSource {
+ const SimCountryCodeSource();
+
+ @override
+ Future resolve() async {
+ try {
+ switch (defaultTargetPlatform) {
+ case TargetPlatform.android:
+ return await _resolveAndroid();
+ case TargetPlatform.iOS:
+ return await _resolveIos();
+ default:
+ // Kein Mobilfunk-Stack auf Desktop/Web-Zielen (auch wenn diese
+ // App laut Scope nur Android/iOS unterstützt, siehe Plan
+ // Abschnitt 7 – defensiv trotzdem abgedeckt).
+ return null;
+ }
+ } catch (_) {
+ return null;
+ }
+ }
+
+ Future _resolveAndroid() async {
+ final info = await CarrierInfo.getAndroidInfo();
+ if (info == null) return null;
+
+ for (final telephony in info.telephonyInfo) {
+ final code = _normalize(telephony.networkCountryIso);
+ if (code != null) return code;
+ }
+ for (final telephony in info.telephonyInfo) {
+ final code = _normalize(telephony.isoCountryCode);
+ if (code != null) return code;
+ }
+ for (final subscription in info.subscriptionsInfo) {
+ final code = _normalize(subscription.countryIso);
+ if (code != null) return code;
+ }
+ return null;
+ }
+
+ Future _resolveIos() async {
+ final info = await CarrierInfo.getIosInfo();
+ for (final carrier in info.carrierData) {
+ final code = _normalize(carrier.isoCountryCode);
+ if (code != null) return code;
+ }
+ return null;
+ }
+
+ String? _normalize(String? code) {
+ final trimmed = code?.trim();
+ if (trimmed == null || trimmed.isEmpty) return null;
+ return trimmed.toUpperCase();
+ }
+}
diff --git a/lib/services/dialer_service.dart b/lib/services/dialer_service.dart
new file mode 100644
index 0000000..41b23fa
--- /dev/null
+++ b/lib/services/dialer_service.dart
@@ -0,0 +1,25 @@
+import 'package:url_launcher/url_launcher.dart';
+
+/// Öffnet die native Telefon-App mit vorgewählter Nummer.
+///
+/// Löst bewusst **kein** Auto-Dial aus (siehe `docs/plan.md`, Abschnitt 2):
+/// Der Nutzer bestätigt den Anruf selbst im Wähler. Wirft keine Exceptions –
+/// wenn das `tel:`-Schema nicht geöffnet werden kann, liefert [call] `false`
+/// zurück und überlässt der UI (AP3+), wie damit umgegangen wird.
+class DialerService {
+ const DialerService();
+
+ /// Baut die `tel:`-URI für [number] und öffnet sie im Wähler.
+ ///
+ /// Gibt `true` zurück, wenn der Wähler erfolgreich geöffnet wurde,
+ /// sonst `false`.
+ Future call(String number) async {
+ final uri = Uri(scheme: 'tel', path: number);
+
+ if (!await canLaunchUrl(uri)) {
+ return false;
+ }
+
+ return launchUrl(uri);
+ }
+}
diff --git a/lib/services/location_service.dart b/lib/services/location_service.dart
new file mode 100644
index 0000000..fb3af83
--- /dev/null
+++ b/lib/services/location_service.dart
@@ -0,0 +1,70 @@
+import 'country_code_sources/cached_country_code_source.dart';
+import 'country_code_sources/country_code_source.dart';
+import 'country_code_sources/gps_country_code_source.dart';
+import 'country_code_sources/manual_country_code_source.dart';
+import 'country_code_sources/sim_country_code_source.dart';
+
+/// Orchestriert die priorisierte Ermittlungskette für den aktuellen
+/// Ländercode (`docs/plan.md`, Abschnitt 6 + 8, AP4 + AP6):
+///
+/// 0. manuelle Auswahl des Nutzers (`shared_preferences`, übersteuert alles)
+/// 1. SIM/Netz-Ländercode (offline, primär)
+/// 2. GPS + Reverse-Geocoding (Fallback, ggf. Netz nötig)
+/// 3. zuletzt gespeicherter Code (`shared_preferences`)
+///
+/// Die erste Quelle, die einen nicht-`null`-Wert liefert, gewinnt; die
+/// übrigen Quellen werden dann nicht mehr angefragt. Nach einem Treffer über
+/// SIM oder GPS wird der Cache best-effort aktualisiert (Fehler dabei
+/// beeinflussen die Rückgabe nicht), damit ein späterer Aufruf ohne
+/// Netz/GPS/SIM trotzdem einen aktuellen Wert liefern kann. Die manuelle
+/// Auswahl (Stufe 0) wird bewusst **nicht** in diesen automatischen Cache
+/// (Stufe 3) geschrieben – beides sind getrennte Mechanismen mit eigenem
+/// `shared_preferences`-Key, siehe `ManualCountryCodeSource` und
+/// `CachedCountryCodeSource`.
+///
+/// `null` wird nur zurückgegeben, wenn wirklich alle Quellen fehlschlagen –
+/// die UI (AP5+) muss diesen Fall (`status = countryUnknown`) explizit
+/// behandeln.
+class LocationService {
+ LocationService({
+ CountryCodeSource? simSource,
+ CountryCodeSource? gpsSource,
+ CountryCodeCache? cacheSource,
+ ManualCountryCodeStore? manualSource,
+ }) : _simSource = simSource ?? const SimCountryCodeSource(),
+ _gpsSource = gpsSource ?? GpsCountryCodeSource(),
+ _cacheSource = cacheSource ?? const CachedCountryCodeSource(),
+ _manualSource = manualSource ?? const ManualCountryCodeSource();
+
+ final CountryCodeSource _simSource;
+ final CountryCodeSource _gpsSource;
+ final CountryCodeCache _cacheSource;
+ final ManualCountryCodeStore _manualSource;
+
+ Future resolveCountryCode() async {
+ final manualResult = await _manualSource.resolve();
+ if (manualResult != null) {
+ return manualResult;
+ }
+
+ final simResult = await _simSource.resolve();
+ if (simResult != null) {
+ await _cacheSource.save(simResult);
+ return simResult;
+ }
+
+ final gpsResult = await _gpsSource.resolve();
+ if (gpsResult != null) {
+ await _cacheSource.save(gpsResult);
+ return gpsResult;
+ }
+
+ return _cacheSource.resolve();
+ }
+
+ /// Setzt oder löscht die manuelle Länderauswahl (`docs/plan.md`,
+ /// Abschnitt 8, AP6). `isoCode == null` löscht eine zuvor gesetzte Auswahl
+ /// wieder, sodass der nächste [resolveCountryCode]-Aufruf wieder auf die
+ /// automatische Kette (SIM → GPS → Cache) zurückfällt.
+ Future setManualCountryCode(String? isoCode) => _manualSource.save(isoCode);
+}
diff --git a/pubspec.lock b/pubspec.lock
index 369cc8a..516e581 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -57,6 +57,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.12.6"
+ carrier_info:
+ dependency: "direct main"
+ description:
+ name: carrier_info
+ sha256: "4bddb1e2b6c9b57a16964890999430dc0c2756aaf4d5a7117308c0852b17dcf4"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.3"
characters:
dependency: transitive
description:
@@ -230,7 +238,7 @@ packages:
source: hosted
version: "1.0.2"
geocoding_platform_interface:
- dependency: transitive
+ dependency: "direct dev"
description:
name: geocoding_platform_interface
sha256: "5164b8871705af7573698b3aa0746337faf818d72b488988741bcc7aeedd2f6e"
@@ -270,7 +278,7 @@ packages:
source: hosted
version: "0.2.6"
geolocator_platform_interface:
- dependency: transitive
+ dependency: "direct dev"
description:
name: geolocator_platform_interface
sha256: cdb082e4f048b69da244117b7914cc60d2a8897546ffaa4f2529c786ded7aee2
@@ -715,7 +723,7 @@ packages:
source: hosted
version: "3.2.5"
url_launcher_platform_interface:
- dependency: transitive
+ dependency: "direct dev"
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
diff --git a/pubspec.yaml b/pubspec.yaml
index b05c602..3bf81dc 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -38,6 +38,7 @@ dependencies:
geolocator: ^14.0.3
geocoding: ^5.0.0
permission_handler: ^12.0.3
+ carrier_info: ^3.0.3
provider: ^6.1.5+1
shared_preferences: ^2.5.5
flutter_localizations:
@@ -48,6 +49,16 @@ dev_dependencies:
flutter_test:
sdk: flutter
+ # Für Unit-Tests von DialerService: erlaubt das Ersetzen von
+ # UrlLauncherPlatform.instance durch eine Fake-Implementierung.
+ url_launcher_platform_interface: ^2.3.2
+
+ # Für Unit-Tests von GpsCountryCodeSource (AP4): erlaubt analog das
+ # Ersetzen von GeolocatorPlatform.instance / GeocodingPlatformFactory.instance
+ # durch Fake-Implementierungen, ohne echte Plattform-Kanäle.
+ geolocator_platform_interface: ^4.2.8
+ geocoding_platform_interface: ^5.0.0
+
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
@@ -55,6 +66,16 @@ dev_dependencies:
# rules and activating additional ones.
flutter_lints: ^6.0.0
+# Abweichung vom Plan nötig (AP4): `carrier_info: ^3.0.3` deklariert
+# `flutter_lints: ^4.0.0` fälschlich als normale (nicht Dev-)Dependency, obwohl
+# das Paket flutter_lints im eigenen lib/-Code gar nicht importiert (nur für
+# die eigene Analyse verwendet). Ohne Override kollidiert das mit unserem
+# `flutter_lints: ^6.0.0`. Der Override ist unkritisch, da flutter_lints rein
+# ein Analyzer-Regelwerk ist und zur Laufzeit von carrier_info nicht genutzt
+# wird.
+dependency_overrides:
+ flutter_lints: ^6.0.0
+
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@@ -70,6 +91,8 @@ flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
+ assets:
+ - assets/data/emergency_numbers.json
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
diff --git a/test/app_locale_test.dart b/test/app_locale_test.dart
new file mode 100644
index 0000000..8eb9d9e
--- /dev/null
+++ b/test/app_locale_test.dart
@@ -0,0 +1,91 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:emergency/app.dart';
+import 'package:emergency/repositories/emergency_number_repository.dart';
+import 'package:emergency/services/country_code_sources/country_code_source.dart';
+import 'package:emergency/services/location_service.dart';
+
+/// Siehe `test/widget_test.dart` für die Begründung dieser Fakes
+/// (deterministische Ländererkennung ohne echte Plattform-Kanäle).
+class _FakeSource implements CountryCodeSource {
+ _FakeSource(this._result);
+
+ final String? _result;
+
+ @override
+ Future resolve() async => _result;
+}
+
+class _FakeCache implements CountryCodeCache {
+ @override
+ Future resolve() async => null;
+
+ @override
+ Future save(String isoCode) async {}
+}
+
+class _FakeManualSource implements ManualCountryCodeStore {
+ @override
+ Future resolve() async => null;
+
+ @override
+ Future save(String? isoCode) async {}
+}
+
+/// AP7: Verifiziert, dass die sichtbare App-Sprache tatsächlich an das
+/// erkannte Land gekoppelt ist (`docs/plan.md`, Abschnitt 2 + 8), nicht nur,
+/// dass `resolveAppLocale` als reine Funktion korrekt rechnet
+/// (`test/core/country_locale_map_test.dart`).
+Future _pumpAppWithCountry(WidgetTester tester, String? isoCode) async {
+ final repository = EmergencyNumberRepository();
+ await tester.runAsync(() => repository.load());
+ final locationService = LocationService(
+ simSource: _FakeSource(isoCode),
+ gpsSource: _FakeSource(null),
+ cacheSource: _FakeCache(),
+ manualSource: _FakeManualSource(),
+ );
+
+ await tester.pumpWidget(App(repository: repository, locationService: locationService));
+ await tester.pumpAndSettle();
+}
+
+/// Findet Text konkret innerhalb der `AppBar`, nicht irgendwo im Baum:
+/// Der Notrufnummern-Datensatz mancher Länder (z. B. DE: "Notruf", US:
+/// "Emergency") verwendet zufällig denselben Text wie der übersetzte
+/// AppBar-Titel, ein ungebundener `find.text(...)` wäre daher mehrdeutig.
+Finder _appBarText(String text) =>
+ find.descendant(of: find.byType(AppBar), matching: find.text(text));
+
+void main() {
+ testWidgets('DE wird erkannt -> AppBar-Titel erscheint auf Deutsch ("Notruf")', (tester) async {
+ await _pumpAppWithCountry(tester, 'DE');
+
+ expect(_appBarText('Notruf'), findsOneWidget);
+ expect(_appBarText('Emergency'), findsNothing);
+ });
+
+ testWidgets('US wird erkannt -> AppBar-Titel erscheint auf Englisch ("Emergency")', (
+ tester,
+ ) async {
+ await _pumpAppWithCountry(tester, 'US');
+
+ expect(_appBarText('Emergency'), findsOneWidget);
+ expect(_appBarText('Notruf'), findsNothing);
+ });
+
+ testWidgets('FR wird erkannt -> AppBar-Titel erscheint auf Französisch ("Urgence")', (
+ tester,
+ ) async {
+ await _pumpAppWithCountry(tester, 'FR');
+
+ expect(_appBarText('Urgence'), findsOneWidget);
+ });
+
+ testWidgets('Land nicht ermittelbar -> Fallback auf Englisch ("Emergency")', (tester) async {
+ await _pumpAppWithCountry(tester, null);
+
+ expect(_appBarText('Emergency'), findsOneWidget);
+ });
+}
diff --git a/test/core/country_locale_map_test.dart b/test/core/country_locale_map_test.dart
new file mode 100644
index 0000000..6aff07c
--- /dev/null
+++ b/test/core/country_locale_map_test.dart
@@ -0,0 +1,44 @@
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:emergency/core/country_locale_map.dart';
+import 'package:emergency/l10n/app_localizations.dart';
+
+void main() {
+ group('resolveAppLocale', () {
+ // Realistische Basis: die tatsächlich per ARB generierten Locales
+ // (docs/plan.md, Abschnitt 8, AP7), statt einer synthetischen Liste.
+ final supportedLocales = AppLocalizations.supportedLocales;
+
+ test('bekanntes Land mit unterstützter Sprache liefert diese Sprache (DE -> de)', () {
+ expect(resolveAppLocale('DE', supportedLocales), const Locale('de'));
+ });
+
+ test('Ländercode ist case-insensitiv (de -> de)', () {
+ expect(resolveAppLocale('de', supportedLocales), const Locale('de'));
+ });
+
+ test(
+ 'bekanntes Land mit einer NICHT unterstützten Sprache fällt auf Englisch zurück (JP -> ja, nicht unterstützt)',
+ () {
+ expect(countryLocaleMap['JP'], 'ja');
+ expect(supportedLocales.any((l) => l.languageCode == 'ja'), isFalse);
+ expect(resolveAppLocale('JP', supportedLocales), const Locale('en'));
+ },
+ );
+
+ test('unbekannter/nicht gemappter Ländercode fällt auf Englisch zurück', () {
+ expect(countryLocaleMap.containsKey('ZZ'), isFalse);
+ expect(resolveAppLocale('ZZ', supportedLocales), const Locale('en'));
+ });
+
+ test('country == null (noch nicht ermittelt) fällt auf Englisch zurück', () {
+ expect(resolveAppLocale(null, supportedLocales), const Locale('en'));
+ });
+
+ test('weitere unterstützte Sprachen werden ebenfalls korrekt aufgelöst (FR -> fr, ES -> es)', () {
+ expect(resolveAppLocale('FR', supportedLocales), const Locale('fr'));
+ expect(resolveAppLocale('ES', supportedLocales), const Locale('es'));
+ });
+ });
+}
diff --git a/test/features/home/country_picker_screen_test.dart b/test/features/home/country_picker_screen_test.dart
new file mode 100644
index 0000000..ba928f7
--- /dev/null
+++ b/test/features/home/country_picker_screen_test.dart
@@ -0,0 +1,146 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:provider/provider.dart';
+
+import 'package:emergency/features/home/country_picker_screen.dart';
+import 'package:emergency/l10n/app_localizations.dart';
+import 'package:emergency/models/country.dart';
+import 'package:emergency/models/emergency_number.dart';
+import 'package:emergency/models/emergency_type.dart';
+import 'package:emergency/providers/emergency_provider.dart';
+import 'package:emergency/repositories/emergency_number_repository.dart';
+import 'package:emergency/services/country_code_sources/country_code_source.dart';
+import 'package:emergency/services/location_service.dart';
+
+/// Liefert eine feste Länderliste, ohne das echte JSON-Asset laden zu
+/// müssen (analog zum Fake-Repository-Muster in `emergency_provider_test.dart`).
+class _FakeRepository extends EmergencyNumberRepository {
+ @override
+ List get allCountries => const [
+ Country(
+ isoCode: 'DE',
+ nameDe: 'Deutschland',
+ nameEn: 'Germany',
+ flag: '🇩🇪',
+ numbers: [
+ EmergencyNumber(type: EmergencyType.general, number: '112', labelDe: 'Notruf', labelEn: 'Emergency'),
+ ],
+ ),
+ Country(
+ isoCode: 'FR',
+ nameDe: 'Frankreich',
+ nameEn: 'France',
+ flag: '🇫🇷',
+ numbers: [
+ EmergencyNumber(type: EmergencyType.general, number: '112', labelDe: 'Notruf', labelEn: 'Emergency'),
+ ],
+ ),
+ ];
+}
+
+class _NoopSource implements CountryCodeSource {
+ const _NoopSource();
+
+ @override
+ Future resolve() async => null;
+}
+
+/// Fake-Provider: überschreibt `selectCountryManually`, damit der Test ohne
+/// echte `LocationService`-Ermittlungskette prüfen kann, mit welchem
+/// ISO-Code der Screen die Auswahl auslöst.
+class _FakeProvider extends EmergencyProvider {
+ _FakeProvider({required super.repository})
+ : super(
+ locationService: LocationService(
+ simSource: const _NoopSource(),
+ gpsSource: const _NoopSource(),
+ ),
+ );
+
+ String? selectedIsoCode;
+ int selectCallCount = 0;
+
+ @override
+ Future selectCountryManually(String isoCode) async {
+ selectCallCount++;
+ selectedIsoCode = isoCode;
+ }
+}
+
+void main() {
+ Future pumpPickerBehindAButton(WidgetTester tester, EmergencyProvider provider) async {
+ // CountryPickerScreen ruft `Navigator.pop()` beim Auswählen auf – dafür
+ // muss eine vorherige Route existieren, sonst wäre der Pop ein No-op.
+ await tester.pumpWidget(
+ ChangeNotifierProvider.value(
+ value: provider,
+ child: MaterialApp(
+ // Feste Locale, damit die (unveränderten) deutschen Text-Assertions
+ // dieser Tests unabhängig von der Test-Umgebungs-/Systemsprache
+ // funktionieren; das eigentliche Umschalten der App-Sprache wird
+ // gezielt in `test/core/country_locale_map_test.dart` bzw.
+ // `test/app_locale_test.dart` (AP7) geprüft.
+ locale: const Locale('de'),
+ localizationsDelegates: AppLocalizations.localizationsDelegates,
+ supportedLocales: AppLocalizations.supportedLocales,
+ home: Builder(
+ builder: (context) => ElevatedButton(
+ onPressed: () => Navigator.of(context).push(
+ MaterialPageRoute(builder: (_) => const CountryPickerScreen()),
+ ),
+ child: const Text('Öffnen'),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ await tester.tap(find.text('Öffnen'));
+ await tester.pumpAndSettle();
+ }
+
+ testWidgets('zeigt die Länderliste an', (tester) async {
+ final provider = _FakeProvider(repository: _FakeRepository());
+ await pumpPickerBehindAButton(tester, provider);
+
+ expect(find.text('Deutschland'), findsOneWidget);
+ expect(find.text('Frankreich'), findsOneWidget);
+ });
+
+ testWidgets('Suchfeld filtert die Länderliste', (tester) async {
+ final provider = _FakeProvider(repository: _FakeRepository());
+ await pumpPickerBehindAButton(tester, provider);
+
+ await tester.enterText(find.byType(TextField), 'Deutsch');
+ await tester.pump();
+
+ expect(find.text('Deutschland'), findsOneWidget);
+ expect(find.text('Frankreich'), findsNothing);
+ });
+
+ testWidgets('zeigt einen Hinweis, wenn kein Land zum Filter passt', (tester) async {
+ final provider = _FakeProvider(repository: _FakeRepository());
+ await pumpPickerBehindAButton(tester, provider);
+
+ await tester.enterText(find.byType(TextField), 'Nichtland');
+ await tester.pump();
+
+ expect(find.text('Kein Land gefunden.'), findsOneWidget);
+ });
+
+ testWidgets('Tap auf ein Land löst selectCountryManually aus und schließt den Screen', (
+ tester,
+ ) async {
+ final provider = _FakeProvider(repository: _FakeRepository());
+ await pumpPickerBehindAButton(tester, provider);
+
+ await tester.tap(find.text('Deutschland'));
+ await tester.pumpAndSettle();
+
+ expect(provider.selectCallCount, 1);
+ expect(provider.selectedIsoCode, 'DE');
+ // Screen wurde geschlossen (zurück zum "Öffnen"-Button).
+ expect(find.text('Öffnen'), findsOneWidget);
+ expect(find.text('Deutschland'), findsNothing);
+ });
+}
diff --git a/test/models/country_test.dart b/test/models/country_test.dart
new file mode 100644
index 0000000..bc068d2
--- /dev/null
+++ b/test/models/country_test.dart
@@ -0,0 +1,57 @@
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:emergency/models/country.dart';
+import 'package:emergency/models/emergency_type.dart';
+
+void main() {
+ group('Country.fromJson', () {
+ test('parst ein Land mit mehreren Notrufnummern korrekt', () {
+ final country = Country.fromJson('DE', {
+ 'name_en': 'Germany',
+ 'name_de': 'Deutschland',
+ 'flag': '🇩🇪',
+ 'numbers': [
+ {
+ 'type': 'general',
+ 'number': '112',
+ 'label_de': 'Notruf',
+ 'label_en': 'Emergency',
+ },
+ {
+ 'type': 'police',
+ 'number': '110',
+ 'label_de': 'Polizei',
+ 'label_en': 'Police',
+ },
+ ],
+ });
+
+ expect(country.isoCode, 'DE');
+ expect(country.nameDe, 'Deutschland');
+ expect(country.nameEn, 'Germany');
+ expect(country.flag, '🇩🇪');
+ expect(country.numbers, hasLength(2));
+ expect(country.numbers[0].type, EmergencyType.general);
+ expect(country.numbers[1].type, EmergencyType.police);
+ });
+
+ test('parst ein Land mit nur einer Notrufnummer korrekt', () {
+ final country = Country.fromJson('US', {
+ 'name_en': 'United States',
+ 'name_de': 'Vereinigte Staaten',
+ 'flag': '🇺🇸',
+ 'numbers': [
+ {
+ 'type': 'general',
+ 'number': '911',
+ 'label_de': 'Notruf',
+ 'label_en': 'Emergency',
+ },
+ ],
+ });
+
+ expect(country.numbers, hasLength(1));
+ expect(country.numbers.single.number, '911');
+ });
+ });
+}
diff --git a/test/models/emergency_number_test.dart b/test/models/emergency_number_test.dart
new file mode 100644
index 0000000..46d594c
--- /dev/null
+++ b/test/models/emergency_number_test.dart
@@ -0,0 +1,40 @@
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:emergency/models/emergency_number.dart';
+import 'package:emergency/models/emergency_type.dart';
+
+void main() {
+ group('EmergencyNumber.fromJson', () {
+ test('parst gültiges JSON korrekt', () {
+ final number = EmergencyNumber.fromJson({
+ 'type': 'police',
+ 'number': '110',
+ 'label_de': 'Polizei',
+ 'label_en': 'Police',
+ });
+
+ expect(number.type, EmergencyType.police);
+ expect(number.number, '110');
+ expect(number.labelDe, 'Polizei');
+ expect(number.labelEn, 'Police');
+ });
+
+ test('zwei inhaltsgleiche Instanzen sind gleich', () {
+ final a = EmergencyNumber.fromJson({
+ 'type': 'general',
+ 'number': '112',
+ 'label_de': 'Notruf',
+ 'label_en': 'Emergency',
+ });
+ final b = EmergencyNumber.fromJson({
+ 'type': 'general',
+ 'number': '112',
+ 'label_de': 'Notruf',
+ 'label_en': 'Emergency',
+ });
+
+ expect(a, equals(b));
+ expect(a.hashCode, equals(b.hashCode));
+ });
+ });
+}
diff --git a/test/models/emergency_type_test.dart b/test/models/emergency_type_test.dart
new file mode 100644
index 0000000..c560714
--- /dev/null
+++ b/test/models/emergency_type_test.dart
@@ -0,0 +1,26 @@
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:emergency/models/emergency_type.dart';
+
+void main() {
+ group('EmergencyType', () {
+ test('fromJson mappt alle bekannten Werte korrekt', () {
+ expect(EmergencyType.fromJson('general'), EmergencyType.general);
+ expect(EmergencyType.fromJson('police'), EmergencyType.police);
+ expect(EmergencyType.fromJson('fire'), EmergencyType.fire);
+ expect(EmergencyType.fromJson('ambulance'), EmergencyType.ambulance);
+ expect(EmergencyType.fromJson('sea_rescue'), EmergencyType.seaRescue);
+ expect(EmergencyType.fromJson('mountain_rescue'), EmergencyType.mountainRescue);
+ });
+
+ test('fromJson wirft FormatException bei unbekanntem Wert', () {
+ expect(() => EmergencyType.fromJson('unknown'), throwsFormatException);
+ });
+
+ test('toJson ist invers zu fromJson', () {
+ for (final type in EmergencyType.values) {
+ expect(EmergencyType.fromJson(type.toJson()), type);
+ }
+ });
+ });
+}
diff --git a/test/providers/emergency_provider_test.dart b/test/providers/emergency_provider_test.dart
new file mode 100644
index 0000000..1dbec44
--- /dev/null
+++ b/test/providers/emergency_provider_test.dart
@@ -0,0 +1,247 @@
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:emergency/models/country.dart';
+import 'package:emergency/models/emergency_number.dart';
+import 'package:emergency/models/emergency_type.dart';
+import 'package:emergency/providers/emergency_provider.dart';
+import 'package:emergency/providers/emergency_status.dart';
+import 'package:emergency/repositories/emergency_number_repository.dart';
+import 'package:emergency/services/country_code_sources/country_code_source.dart';
+import 'package:emergency/services/location_service.dart';
+
+/// Einfache manuelle Fakes statt Mocking-Package (siehe AP4-Vorgabe: kein
+/// neues Mocking-Package einführen). Beide Basisklassen sind nicht `final`/
+/// `sealed`, daher genügt es, `load`/`lookup` bzw. `resolveCountryCode` zu
+/// überschreiben, statt ein eigenes Interface einzuführen.
+class _FakeRepository extends EmergencyNumberRepository {
+ _FakeRepository({this.loadError, this.countryToReturn});
+
+ Object? loadError;
+ Country? countryToReturn;
+ int loadCallCount = 0;
+
+ @override
+ Future load() async {
+ loadCallCount++;
+ final error = loadError;
+ if (error != null) throw error;
+ }
+
+ @override
+ Country? lookup(String isoCode) => countryToReturn;
+}
+
+/// Nur zur Erfüllung des `super()`-Konstruktors von [LocationService] nötig
+/// (wird nie aufgerufen, da [_FakeLocationService.resolveCountryCode]
+/// überschrieben ist). Ohne einen expliziten `gpsSource` würde
+/// `LocationService()` intern ein echtes `GpsCountryCodeSource` (und damit
+/// `Geocoding()`) konstruieren, was außerhalb eines registrierten
+/// Platform-Interface (siehe `gps_country_code_source_test.dart`) sofort wirft.
+class _NoopSource implements CountryCodeSource {
+ const _NoopSource();
+
+ @override
+ Future resolve() async => null;
+}
+
+class _FakeLocationService extends LocationService {
+ _FakeLocationService(this.isoCode)
+ : super(simSource: const _NoopSource(), gpsSource: const _NoopSource());
+
+ /// Simuliert das Ergebnis der automatischen Kette (SIM → GPS → Cache).
+ String? isoCode;
+
+ /// Simuliert die manuelle Auswahl (AP6): hat, analog zur echten
+ /// Priorisierung in `LocationService`, Vorrang vor [isoCode].
+ String? manualIsoCode;
+
+ int resolveCallCount = 0;
+ int setManualCallCount = 0;
+ String? lastManualCode;
+
+ @override
+ Future resolveCountryCode() async {
+ resolveCallCount++;
+ return manualIsoCode ?? isoCode;
+ }
+
+ @override
+ Future setManualCountryCode(String? code) async {
+ setManualCallCount++;
+ lastManualCode = code;
+ manualIsoCode = code;
+ }
+}
+
+const _germany = Country(
+ isoCode: 'DE',
+ nameDe: 'Deutschland',
+ nameEn: 'Germany',
+ flag: '🇩🇪',
+ numbers: [
+ EmergencyNumber(type: EmergencyType.general, number: '112', labelDe: 'Notruf', labelEn: 'Emergency'),
+ ],
+);
+
+const _france = Country(
+ isoCode: 'FR',
+ nameDe: 'Frankreich',
+ nameEn: 'France',
+ flag: '🇫🇷',
+ numbers: [
+ EmergencyNumber(type: EmergencyType.general, number: '112', labelDe: 'Notruf', labelEn: 'Emergency'),
+ ],
+);
+
+void main() {
+ group('EmergencyProvider.init()', () {
+ test('erfolgreicher Durchlauf -> status ready mit Land', () async {
+ final provider = EmergencyProvider(
+ repository: _FakeRepository(countryToReturn: _germany),
+ locationService: _FakeLocationService('DE'),
+ );
+
+ await provider.init();
+
+ expect(provider.status, EmergencyStatus.ready);
+ expect(provider.country, _germany);
+ expect(provider.errorMessage, isNull);
+ });
+
+ test('Ländercode nicht ermittelbar -> status countryUnknown', () async {
+ final provider = EmergencyProvider(
+ repository: _FakeRepository(countryToReturn: _germany),
+ locationService: _FakeLocationService(null),
+ );
+
+ await provider.init();
+
+ expect(provider.status, EmergencyStatus.countryUnknown);
+ expect(provider.country, isNull);
+ });
+
+ test('Land nicht in Datenbasis -> status ready mit country == null', () async {
+ final provider = EmergencyProvider(
+ repository: _FakeRepository(countryToReturn: null),
+ locationService: _FakeLocationService('XX'),
+ );
+
+ await provider.init();
+
+ expect(provider.status, EmergencyStatus.ready);
+ expect(provider.country, isNull);
+ });
+
+ test('repository.load() wirft -> status error mit errorMessage', () async {
+ final repository = _FakeRepository(loadError: const FormatException('kaputtes Asset'));
+ final locationService = _FakeLocationService('DE');
+ final provider = EmergencyProvider(repository: repository, locationService: locationService);
+
+ await provider.init();
+
+ expect(provider.status, EmergencyStatus.error);
+ expect(provider.errorMessage, isNotNull);
+ expect(repository.loadCallCount, 1);
+ // Ländererkennung darf nach einem Ladefehler nicht mehr angestoßen werden.
+ expect(locationService.resolveCallCount, 0);
+ });
+
+ test('durchläuft die Status initializing -> locating -> ready', () async {
+ final provider = EmergencyProvider(
+ repository: _FakeRepository(countryToReturn: _germany),
+ locationService: _FakeLocationService('DE'),
+ );
+
+ final observedStatuses = [];
+ provider.addListener(() => observedStatuses.add(provider.status));
+
+ await provider.init();
+
+ expect(observedStatuses, [
+ EmergencyStatus.initializing,
+ EmergencyStatus.locating,
+ EmergencyStatus.ready,
+ ]);
+ });
+ });
+
+ group('EmergencyProvider.retry()', () {
+ test('wiederholt init() und kann nach einem Fehler erfolgreich sein', () async {
+ final repository = _FakeRepository(
+ loadError: const FormatException('kaputtes Asset'),
+ countryToReturn: _germany,
+ );
+ final locationService = _FakeLocationService('DE');
+ final provider = EmergencyProvider(repository: repository, locationService: locationService);
+
+ await provider.init();
+ expect(provider.status, EmergencyStatus.error);
+ expect(repository.loadCallCount, 1);
+
+ repository.loadError = null;
+ await provider.retry();
+
+ expect(provider.status, EmergencyStatus.ready);
+ expect(provider.country, _germany);
+ expect(provider.errorMessage, isNull);
+ expect(repository.loadCallCount, 2);
+ });
+ });
+
+ group('EmergencyProvider.selectCountryManually()', () {
+ test('persistiert die Auswahl und stößt init() erneut an', () async {
+ final repository = _FakeRepository(countryToReturn: _germany);
+ final locationService = _FakeLocationService('DE');
+ final provider = EmergencyProvider(repository: repository, locationService: locationService);
+
+ await provider.init();
+ expect(provider.country, _germany);
+
+ repository.countryToReturn = _france;
+ await provider.selectCountryManually('FR');
+
+ expect(locationService.setManualCallCount, 1);
+ expect(locationService.lastManualCode, 'FR');
+ // init() wurde ein zweites Mal komplett durchlaufen.
+ expect(locationService.resolveCallCount, 2);
+ expect(provider.status, EmergencyStatus.ready);
+ expect(provider.country, _france);
+ });
+ });
+
+ group('EmergencyProvider.clearManualSelection()', () {
+ test('löscht die manuelle Auswahl und ermittelt danach erneut automatisch', () async {
+ // isoCode simuliert das Ergebnis der automatischen Kette (z. B. SIM),
+ // manualIsoCode eine bereits zuvor gesetzte manuelle Auswahl.
+ final locationService = _FakeLocationService('DE')..manualIsoCode = 'FR';
+ final repository = _FakeRepository(countryToReturn: _france);
+ final provider = EmergencyProvider(repository: repository, locationService: locationService);
+
+ await provider.init();
+ expect(provider.country, _france); // manuelle Auswahl hat Vorrang
+
+ repository.countryToReturn = _germany;
+ await provider.clearManualSelection();
+
+ expect(locationService.setManualCallCount, 1);
+ expect(locationService.lastManualCode, isNull);
+ expect(locationService.manualIsoCode, isNull);
+ expect(locationService.resolveCallCount, 2);
+ expect(provider.status, EmergencyStatus.ready);
+ // Nach dem Löschen greift wieder das automatische Ergebnis (DE).
+ expect(provider.country, _germany);
+ });
+ });
+
+ group('EmergencyProvider.repository', () {
+ test('exponiert das intern verwendete Repository', () {
+ final repository = _FakeRepository(countryToReturn: _germany);
+ final provider = EmergencyProvider(
+ repository: repository,
+ locationService: _FakeLocationService('DE'),
+ );
+
+ expect(provider.repository, same(repository));
+ });
+ });
+}
diff --git a/test/repositories/emergency_number_repository_test.dart b/test/repositories/emergency_number_repository_test.dart
new file mode 100644
index 0000000..346d8f7
--- /dev/null
+++ b/test/repositories/emergency_number_repository_test.dart
@@ -0,0 +1,97 @@
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:emergency/repositories/emergency_number_repository.dart';
+
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ group('EmergencyNumberRepository', () {
+ test('lookup() vor load() liefert null statt Exception', () {
+ final repository = EmergencyNumberRepository();
+ expect(repository.isLoaded, isFalse);
+ expect(repository.lookup('DE'), isNull);
+ });
+
+ test('load() liest das Asset ein und lookup() liefert bekannte Länder', () async {
+ final repository = EmergencyNumberRepository();
+ await repository.load();
+
+ expect(repository.isLoaded, isTrue);
+
+ final germany = repository.lookup('DE');
+ expect(germany, isNotNull);
+ expect(germany!.nameEn, 'Germany');
+ expect(germany.nameDe, 'Deutschland');
+ expect(germany.numbers.map((e) => e.number), containsAll(['112', '110']));
+
+ final us = repository.lookup('US');
+ expect(us, isNotNull);
+ expect(us!.nameEn, 'United States');
+ expect(us.numbers.single.number, '911');
+
+ final uk = repository.lookup('GB');
+ expect(uk, isNotNull);
+ expect(uk!.nameEn, 'United Kingdom');
+ expect(uk.numbers.map((e) => e.number), contains('999'));
+ });
+
+ test('lookup() ist case-insensitive', () async {
+ final repository = EmergencyNumberRepository();
+ await repository.load();
+
+ final lower = repository.lookup('de');
+ final mixed = repository.lookup('De');
+ final upper = repository.lookup('DE');
+
+ expect(lower, isNotNull);
+ expect(lower, equals(upper));
+ expect(mixed, equals(upper));
+ });
+
+ test('lookup() mit unbekanntem Ländercode liefert null', () async {
+ final repository = EmergencyNumberRepository();
+ await repository.load();
+
+ expect(repository.lookup('ZZ'), isNull);
+ expect(repository.lookup(''), isNull);
+ });
+
+ test('load() ist idempotent (zweiter Aufruf lädt nicht erneut)', () async {
+ final repository = EmergencyNumberRepository();
+ await repository.load();
+ final firstLookup = repository.lookup('DE');
+
+ await repository.load();
+ final secondLookup = repository.lookup('DE');
+
+ expect(firstLookup, equals(secondLookup));
+ });
+ });
+
+ group('EmergencyNumberRepository.allCountries', () {
+ test('vor load() liefert eine leere Liste statt Exception', () {
+ final repository = EmergencyNumberRepository();
+ expect(repository.allCountries, isEmpty);
+ });
+
+ test('nach load() nicht leer und enthält bekannte Länder (DE/US)', () async {
+ final repository = EmergencyNumberRepository();
+ await repository.load();
+
+ final countries = repository.allCountries;
+
+ expect(countries, isNotEmpty);
+ expect(countries.map((c) => c.isoCode), containsAll(['DE', 'US']));
+ });
+
+ test('ist alphabetisch nach nameDe sortiert', () async {
+ final repository = EmergencyNumberRepository();
+ await repository.load();
+
+ final names = repository.allCountries.map((c) => c.nameDe).toList();
+ final sortedNames = [...names]..sort();
+
+ expect(names, equals(sortedNames));
+ });
+ });
+}
diff --git a/test/services/country_code_sources/cached_country_code_source_test.dart b/test/services/country_code_sources/cached_country_code_source_test.dart
new file mode 100644
index 0000000..15fc9bb
--- /dev/null
+++ b/test/services/country_code_sources/cached_country_code_source_test.dart
@@ -0,0 +1,48 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+import 'package:emergency/services/country_code_sources/cached_country_code_source.dart';
+
+/// Im Gegensatz zu den SIM-/GPS-Quellen ist `shared_preferences` über
+/// `SharedPreferences.setMockInitialValues` vollständig ohne echten
+/// Plattform-Kanal testbar – daher hier ein echter Verhaltenstest statt nur
+/// eines Struktur-Tests.
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ group('CachedCountryCodeSource', () {
+ setUp(() {
+ SharedPreferences.setMockInitialValues({});
+ });
+
+ test('resolve() liefert null, wenn noch kein Code gespeichert ist', () async {
+ const source = CachedCountryCodeSource();
+
+ expect(await source.resolve(), isNull);
+ });
+
+ test('save() gefolgt von resolve() liefert den gespeicherten, normalisierten Code', () async {
+ const source = CachedCountryCodeSource();
+
+ await source.save('de');
+
+ expect(await source.resolve(), 'DE');
+ });
+
+ test('resolve() liefert null, wenn der gespeicherte Wert leer ist', () async {
+ SharedPreferences.setMockInitialValues({'last_known_country_code': ' '});
+ const source = CachedCountryCodeSource();
+
+ expect(await source.resolve(), isNull);
+ });
+
+ test('save() überschreibt einen zuvor gespeicherten Code', () async {
+ const source = CachedCountryCodeSource();
+
+ await source.save('DE');
+ await source.save('fr');
+
+ expect(await source.resolve(), 'FR');
+ });
+ });
+}
diff --git a/test/services/country_code_sources/gps_country_code_source_test.dart b/test/services/country_code_sources/gps_country_code_source_test.dart
new file mode 100644
index 0000000..5cf6c68
--- /dev/null
+++ b/test/services/country_code_sources/gps_country_code_source_test.dart
@@ -0,0 +1,171 @@
+import 'package:flutter/widgets.dart' show Locale;
+import 'package:flutter_test/flutter_test.dart';
+import 'package:geocoding_platform_interface/geocoding_platform_interface.dart' as pi;
+import 'package:geolocator_platform_interface/geolocator_platform_interface.dart';
+
+import 'package:emergency/services/country_code_sources/gps_country_code_source.dart';
+
+/// Analog zum `url_launcher`-Test von `DialerService`: `geolocator` und
+/// `geocoding` erlauben es, ihre `*Platform.instance` durch eine eigene
+/// Implementierung zu ersetzen – der offiziell vorgesehene Weg, diese
+/// Packages ohne echten Plattform-Kanal zu testen.
+class _FakeGeolocatorPlatform extends GeolocatorPlatform {
+ _FakeGeolocatorPlatform({
+ required this.permission,
+ this.position,
+ this.currentPositionError,
+ });
+
+ LocationPermission permission;
+ Position? position;
+ Object? currentPositionError;
+
+ @override
+ Future checkPermission() async => permission;
+
+ @override
+ Future requestPermission() async => permission;
+
+ @override
+ Future getCurrentPosition({LocationSettings? locationSettings}) async {
+ if (currentPositionError != null) throw currentPositionError!;
+ return position!;
+ }
+}
+
+class _FakeGeocodingPlatform extends pi.Geocoding {
+ _FakeGeocodingPlatform({this.placemarks = const [], this.error})
+ : super.implementation(const pi.GeocodingCreationParams());
+
+ final List placemarks;
+ Object? error;
+
+ @override
+ Future> placemarkFromCoordinates(
+ double latitude,
+ double longitude, {
+ Locale? locale,
+ }) async {
+ if (error != null) throw error!;
+ return placemarks;
+ }
+}
+
+class _FakeGeocodingPlatformFactory extends pi.GeocodingPlatformFactory {
+ _FakeGeocodingPlatformFactory(this.geocoding);
+
+ final pi.Geocoding geocoding;
+
+ @override
+ pi.Geocoding createGeocoding(pi.GeocodingCreationParams params) => geocoding;
+}
+
+Position _somePosition() => Position(
+ latitude: 52.5,
+ longitude: 13.4,
+ timestamp: DateTime.utc(2026, 1, 1),
+ accuracy: 0,
+ altitude: 0,
+ altitudeAccuracy: 0,
+ heading: 0,
+ headingAccuracy: 0,
+ speed: 0,
+ speedAccuracy: 0,
+ );
+
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ void setUpPlatforms({
+ required LocationPermission permission,
+ Position? position,
+ Object? currentPositionError,
+ List placemarks = const [],
+ Object? geocodingError,
+ }) {
+ GeolocatorPlatform.instance = _FakeGeolocatorPlatform(
+ permission: permission,
+ position: position,
+ currentPositionError: currentPositionError,
+ );
+ pi.GeocodingPlatformFactory.instance = _FakeGeocodingPlatformFactory(
+ _FakeGeocodingPlatform(placemarks: placemarks, error: geocodingError),
+ );
+ }
+
+ group('GpsCountryCodeSource', () {
+ test('liefert den ISO-Code aus dem ersten Placemark bei erteilter Berechtigung', () async {
+ setUpPlatforms(
+ permission: LocationPermission.whileInUse,
+ position: _somePosition(),
+ placemarks: const [pi.Placemark(isoCountryCode: 'de')],
+ );
+
+ final source = GpsCountryCodeSource();
+
+ expect(await source.resolve(), 'DE');
+ });
+
+ test('fordert die Berechtigung an, wenn sie noch nicht erteilt ist', () async {
+ setUpPlatforms(
+ permission: LocationPermission.denied,
+ position: _somePosition(),
+ placemarks: const [pi.Placemark(isoCountryCode: 'fr')],
+ );
+
+ final source = GpsCountryCodeSource();
+
+ // Der Fake liefert bei checkPermission() und requestPermission() denselben
+ // Wert; hier reicht es zu prüfen, dass bei "denied" nachgefragt wird und
+ // trotzdem kein Ergebnis zustande kommt (Fake bleibt bei "denied").
+ expect(await source.resolve(), isNull);
+ });
+
+ test('liefert null, wenn die Berechtigung dauerhaft verweigert ist', () async {
+ setUpPlatforms(
+ permission: LocationPermission.deniedForever,
+ position: _somePosition(),
+ placemarks: const [pi.Placemark(isoCountryCode: 'de')],
+ );
+
+ final source = GpsCountryCodeSource();
+
+ expect(await source.resolve(), isNull);
+ });
+
+ test('liefert null, wenn getCurrentPosition wirft (z. B. GPS aus)', () async {
+ setUpPlatforms(
+ permission: LocationPermission.whileInUse,
+ currentPositionError: const LocationServiceDisabledException(),
+ );
+
+ final source = GpsCountryCodeSource();
+
+ expect(await source.resolve(), isNull);
+ });
+
+ test('liefert null, wenn das Reverse-Geocoding wirft (z. B. kein Netz)', () async {
+ setUpPlatforms(
+ permission: LocationPermission.whileInUse,
+ position: _somePosition(),
+ geocodingError: Exception('network error'),
+ );
+
+ final source = GpsCountryCodeSource();
+
+ expect(await source.resolve(), isNull);
+ });
+
+ test('liefert null, wenn keine Placemarks gefunden werden', () async {
+ setUpPlatforms(
+ permission: LocationPermission.whileInUse,
+ position: _somePosition(),
+ placemarks: const [],
+ );
+
+ final source = GpsCountryCodeSource();
+
+ expect(await source.resolve(), isNull);
+ });
+ });
+}
diff --git a/test/services/country_code_sources/manual_country_code_source_test.dart b/test/services/country_code_sources/manual_country_code_source_test.dart
new file mode 100644
index 0000000..dfc7be9
--- /dev/null
+++ b/test/services/country_code_sources/manual_country_code_source_test.dart
@@ -0,0 +1,50 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+import 'package:emergency/services/country_code_sources/manual_country_code_source.dart';
+
+/// Analog zu `cached_country_code_source_test.dart`: `shared_preferences`
+/// ist über `SharedPreferences.setMockInitialValues` vollständig ohne echten
+/// Plattform-Kanal testbar.
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ group('ManualCountryCodeSource', () {
+ setUp(() {
+ SharedPreferences.setMockInitialValues({});
+ });
+
+ test('resolve() liefert null, wenn noch keine manuelle Auswahl gespeichert ist', () async {
+ const source = ManualCountryCodeSource();
+
+ expect(await source.resolve(), isNull);
+ });
+
+ test('save() gefolgt von resolve() liefert den gespeicherten, normalisierten Code', () async {
+ const source = ManualCountryCodeSource();
+
+ await source.save('fr');
+
+ expect(await source.resolve(), 'FR');
+ });
+
+ test('save(null) löscht eine zuvor gesetzte manuelle Auswahl wieder', () async {
+ const source = ManualCountryCodeSource();
+
+ await source.save('FR');
+ expect(await source.resolve(), 'FR');
+
+ await source.save(null);
+ expect(await source.resolve(), isNull);
+ });
+
+ test('verwendet einen eigenen Key, unabhängig vom automatischen Cache', () async {
+ SharedPreferences.setMockInitialValues({'last_known_country_code': 'US'});
+ const source = ManualCountryCodeSource();
+
+ // Der automatische Cache-Wert darf die manuelle Auswahl nicht
+ // beeinflussen, solange keine manuelle Auswahl gesetzt wurde.
+ expect(await source.resolve(), isNull);
+ });
+ });
+}
diff --git a/test/services/country_code_sources/sim_country_code_source_test.dart b/test/services/country_code_sources/sim_country_code_source_test.dart
new file mode 100644
index 0000000..bd247d4
--- /dev/null
+++ b/test/services/country_code_sources/sim_country_code_source_test.dart
@@ -0,0 +1,163 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:emergency/services/country_code_sources/sim_country_code_source.dart';
+
+/// `carrier_info` spricht direkt (ohne Platform-Interface-Package) den
+/// MethodChannel `plugins.chizi.tech/carrier_info` an. Flutter erlaubt es,
+/// diesen Kanal in Tests mit einem Mock-Handler zu belegen – dadurch lässt
+/// sich die eigentliche Priorisierungs-/Normalisierungslogik von
+/// [SimCountryCodeSource] (anders als bei GPS, siehe dortigen Struktur-Test)
+/// echt testen, ganz ohne reales Gerät/Emulator.
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ const channel = MethodChannel('plugins.chizi.tech/carrier_info');
+
+ void mockHandler(Future