From 68ec93a19f8c8eb8b692a1b81a964e6d5fdd4462 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Fri, 26 Jun 2026 15:25:35 -0700 Subject: [PATCH 01/26] Updated translations from lokalise on Fri Jun 26 12:54:46 PDT 2026 --- Common/ko.lproj/Intents.strings | 30 ++- .../Bootstrap/Localizable.xcstrings | 24 ++ Loop/Localizable.xcstrings | 236 +++++++++++++++++- LoopCore/Localizable.xcstrings | 132 ++++++++++ 4 files changed, 407 insertions(+), 15 deletions(-) diff --git a/Common/ko.lproj/Intents.strings b/Common/ko.lproj/Intents.strings index 853af215c0..69202aa99c 100644 --- a/Common/ko.lproj/Intents.strings +++ b/Common/ko.lproj/Intents.strings @@ -1,24 +1,36 @@ -"80eo5o" = "Add Carb Entry"; - +/* (No Comment) */ "9KhaIS" = "I've set the preset"; -"I4OZy8" = "Enable Override Preset"; - -"OcNxIj" = "Add Carb Entry"; - -"XNNmtH" = "Enable preset in Loop"; - -"ZZ3mtM" = "Enable an override preset in Loop"; +/* (No Comment) */ +"80eo5o" = "Add Carb Entry"; +/* (No Comment) */ "b085BW" = "I wasn't able to set the preset."; +/* (No Comment) */ +"I4OZy8" = "Enable Override Preset"; + +/* (No Comment) */ "lYMuWV" = "Override Name"; +/* (No Comment) */ "nDKAmn" = "What's the name of the override you'd like to set?"; +/* (No Comment) */ +"OcNxIj" = "Add Carb Entry"; + +/* (No Comment) */ "oLQSsJ" = "Enable '${overrideName}' Override Preset"; +/* (No Comment) */ +"XNNmtH" = "Enable preset in Loop"; + +/* (No Comment) */ "yBzwCL" = "Override Selection"; +/* (No Comment) */ "yc02Yq" = "Add a carb entry to Loop"; +/* (No Comment) */ +"ZZ3mtM" = "Enable an override preset in Loop"; + diff --git a/Loop Widget Extension/Bootstrap/Localizable.xcstrings b/Loop Widget Extension/Bootstrap/Localizable.xcstrings index b6b231104c..5bb04e12b8 100644 --- a/Loop Widget Extension/Bootstrap/Localizable.xcstrings +++ b/Loop Widget Extension/Bootstrap/Localizable.xcstrings @@ -618,6 +618,12 @@ "value" : "Couleur" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kleur" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -640,6 +646,12 @@ "value" : "Date" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datum" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -781,6 +793,12 @@ "value" : "Fin" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stop" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -1633,6 +1651,12 @@ "value" : "Démarrer" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start" + } + }, "ro" : { "stringUnit" : { "state" : "translated", diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 44d94c62a4..0fe4d44399 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -3320,7 +3320,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "15分钟葡萄糖回归系数(b1),持续30分钟衰减" + "value" : "15分钟血糖回归系数 (b₁),持续30分钟衰减" } } } @@ -7439,6 +7439,12 @@ "value" : "Eple" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Appel" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -13266,6 +13272,12 @@ "Configure Display" : { "comment" : "Title for the view to configure the lock screen display", "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scherm Configureren" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -16377,6 +16389,18 @@ "Display colors for glucose" : { "comment" : "Title for glucose coloring", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Farben für Blutzucker anzeigen" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher les couleurs du glucose" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -16395,6 +16419,12 @@ "comment" : "A section header in the live activity settings view, describing the control options for displaying glucose levels on the live activity screen.", "isCommentAutoGenerated" : true, "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Options de contrôle de l'affichage" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -16406,6 +16436,18 @@ "Display prediction in plot" : { "comment" : "Title for prediction line toggle", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "BZ-Vorhersage im Diagramm anzeigen" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher la prédiction dans le graphique" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -16423,6 +16465,18 @@ "Display up to 4 items. Display label is in parentheses." : { "comment" : "Indicates the maximum number of items that can be displayed and how the label for each item is shortened.", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es werden bis zu 4 Elemente angezeigt. Die Bezeichnungen werden in Klammern angegeben." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Affiche jusqu'à 4 éléments. La forme abrégée du libellé figure entre parenthèses." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -16990,6 +17044,12 @@ "value" : "Activé" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingeschakeld" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -21020,7 +21080,33 @@ } }, "I'm a tester" : { - "comment" : "Button that dismisses the development build warning" + "comment" : "Button that dismisses the development build warning", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ich bin ein Tester" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je suis un testeur" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ik ben een tester" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunt un tester" + } + } + } }, "If iOS Focus Mode is ON and Mute Alerts is OFF, Critical Alerts will still be delivered and non-Critical Alerts will be silenced until %1$@ is added to each Focus mode as an Allowed App." : { "comment" : "Focus modes descriptive text (1: app name)", @@ -23880,6 +23966,12 @@ "value" : "Activité en direct" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Live Activiteit" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -24017,6 +24109,18 @@ "comment" : "The title of a section in the live activity settings view, related to lock screen, dynamic island, or carplay.", "isCommentAutoGenerated" : true, "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sperrbildschirm / Dynamic Island / CarPlay" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Écran verrouillé / Île dynamique / CarPlay" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -25777,7 +25881,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "餐时注射" + "value" : "餐食大剂量" } } } @@ -30970,6 +31074,12 @@ "value" : "Pumpen er utløpt" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pomp Verlopen" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -33205,6 +33315,12 @@ "value" : "Lagre" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opslaan" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -33495,6 +33611,18 @@ "comment" : "A section header for the lock screen display options.", "isCommentAutoGenerated" : true, "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Optionen für die Sperrbildschirm-Anzeige auswählen" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Options d'affichage de l'écran verrouillé" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -35143,7 +35271,33 @@ } }, "Switch to main" : { - "comment" : "Button on the development build warning that opens documentation about switching branches" + "comment" : "Button on the development build warning that opens documentation about switching branches", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zu main wechseln" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basculer sur main" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Overschakelen naar main-branch" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Treceți la principală" + } + } + } }, "Tap here to set up a CGM" : { "comment" : "Descriptive text for button to add CGM device", @@ -35872,6 +36026,12 @@ "value" : "TestFlight" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "TestFlight" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -35925,6 +36085,12 @@ "value" : "TestFlight-utløp" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "TestFlight Verloopdatum" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -35978,6 +36144,12 @@ "value" : "TestFlight utløper " } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "TestFlight verloopt " + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -36037,6 +36209,12 @@ "value" : "TestFlight utløper snart" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "TestFlight verloopt binnenkort" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -37022,7 +37200,33 @@ } }, "This is the development version of Loop, built from the dev branch. Any updates on this branch may contain new, untested features, and may be unsafe. If you are not a tester, please do not use this branch, and switch to main." : { - "comment" : "Body of the warning shown at launch on development builds" + "comment" : "Body of the warning shown at launch on development builds", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dies ist die Entwicklungsversion von Loop, erstellt aus dem Dev-Branch. Aktualisierungen dieses Branchs können neue, ungetestete Funktionen enthalten und möglicherweise unsicher sein. Wenn du kein Tester bist, verwende diesen Zweig bitte nicht, sondern wechseln Sie zum main-Branch." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il s'agit de la version de développement de Loop, compilée à partir de la branche « dev ». Toute mise à jour de cette branche peut contenir de nouvelles fonctionnalités non testées et pourrait présenter des risques. Si vous n'êtes pas testeur, veuillez ne pas utiliser cette branche et passer plutôt à la branche « main »." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dit is de ontwikkelingsversie van Loop, samengesteld op basis van de dev-branch. Updates op deze branch kunnen nieuwe, nog ongeteste functies bevatten en mogelijk onveilig zijn. Als je geen tester bent, gebruik deze branch dan alsjeblieft niet en schakel over naar de main-branch." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aceasta este versiunea de dezvoltare a Loop, construită din ramura dev. Orice actualizări ale acestei ramuri pot conține funcții noi, netestate și pot fi nesigure. Dacă nu sunteți tester, vă rugăm să nu utilizați această ramură și să treceți la ramura principală." + } + } + } }, "This option only applies when Loop's Dosing Strategy is set to Automatic Bolus." : { "comment" : "String shown when glucose based partial application cannot be enabled because dosing strategy is not set to Automatic Bolus", @@ -39568,7 +39772,27 @@ } }, "Warning" : { - "comment" : "Title of the warning shown at launch on development builds" + "comment" : "Title of the warning shown at launch on development builds", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Warnung" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Waarschuwing" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avertizare" + } + } + } }, "Warning! Safety notifications are turned OFF" : { "comment" : "Alert Permissions Need Attention alert title", diff --git a/LoopCore/Localizable.xcstrings b/LoopCore/Localizable.xcstrings index 9c2e507b20..a3ccfb14d6 100644 --- a/LoopCore/Localizable.xcstrings +++ b/LoopCore/Localizable.xcstrings @@ -143,6 +143,12 @@ "value" : "Glucides actifs" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actieve Koolhydraten" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -160,6 +166,12 @@ "Active Carbohydrates (COB)" : { "comment" : "Description for the Carbohydrates On Board selection for the Live Activity configuration", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktive Kohlenhydrate (COB)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -191,6 +203,12 @@ "value" : "Glucides actifs" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actieve Koolhydraten" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -222,6 +240,12 @@ "value" : "Insuline active" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actieve Insuline" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -256,6 +280,12 @@ "at" : { "comment" : "Label used for the Updated time value in the Live Activity view", "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "à" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -368,6 +398,12 @@ "value" : "Basal" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basaal" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -408,6 +444,12 @@ "value" : "Glycémie actuelle" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Huidige BG" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -439,6 +481,12 @@ "value" : "Glycémie actuelle" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Huidige Glucose" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -519,6 +567,12 @@ "value" : "Glycémie éventuelle" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uiteindelijke BG" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -550,6 +604,12 @@ "value" : "Éventuellement" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eventueel" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -567,6 +627,12 @@ "Forecasted Glucose (Eventual BG)" : { "comment" : "Description for the Forecasted Glucose selection for the Live Activity configuration", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prognostizierter Glukosewert (Endgültiger Blutzuckerwert)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -591,6 +657,12 @@ "value" : "Dans quel mode voulez-vous afficher les Activités en direct" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "In which mode do you want to render the Live Activity" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -608,6 +680,18 @@ "IOB" : { "comment" : "Label used for the Insulin On Board value in the Live Activity view", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAB" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -633,6 +717,12 @@ "value" : "Grand" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Groot" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -650,6 +740,18 @@ "Plot and Row" : { "comment" : "Short name to choose the Lock Screen display including the the plot", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diagramm und Zeile" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ligne et graphique" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -667,6 +769,12 @@ "Relative Basal Rate (Basal)" : { "comment" : "Description for the Basal Rate plot selection for the Live Activity configuration", "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Débit basal relatif (Basal)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -678,6 +786,12 @@ "Row Only" : { "comment" : "Short name to choose the Lock Screen display without the plot", "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ligne seule" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -709,6 +823,12 @@ "value" : "Petit" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Klein" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -852,6 +972,18 @@ "value" : "Aktualisiert (am)" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mis à jour (à)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bijgewerkt (op)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", From fa3b3cd85fcd3619c19f22a9a5963cfa8a3172f6 Mon Sep 17 00:00:00 2001 From: loopkitdev Date: Mon, 20 Jul 2026 13:18:25 -0500 Subject: [PATCH 02/26] LiveActivityManager: fix cooperative-pool starvation deadlock (#2465) getGlucoseSample() and getInsulinOnBoard() bridged async work to sync via DispatchGroup.wait(timeout: .distantFuture). Both run from update(), which executes on a Swift Concurrency cooperative-pool thread. Blocking that thread violates the pool's forward-progress guarantee: the pool is sized to the core count, so repeated Live Activity updates park every cooperative thread on the wait and deadlock the whole concurrency runtime (Loop stops looping). Observed on device: all cooperative-pool threads stuck in getGlucoseSample -> dispatch_group_wait, with the Nightscout upload queues blocked behind them. Make both methods async and suspend via withCheckedContinuation instead of blocking. getBottomRow() becomes async and iterates so the IOB query stays lazy (issued only for the .iob row). No behavior change beyond not blocking. Co-authored-by: Pete Schwamb --- .../Live Activity/LiveActivityManager.swift | 112 ++++++++---------- 1 file changed, 52 insertions(+), 60 deletions(-) diff --git a/Loop/Managers/Live Activity/LiveActivityManager.swift b/Loop/Managers/Live Activity/LiveActivityManager.swift index f7a4723f14..5d9122c183 100644 --- a/Loop/Managers/Live Activity/LiveActivityManager.swift +++ b/Loop/Managers/Live Activity/LiveActivityManager.swift @@ -103,7 +103,7 @@ class LiveActivityManager : LiveActivityManagerProxy { let statusContext = UserDefaults.appGroup?.statusExtensionContext let glucoseFormatter = NumberFormatter.glucoseFormatter(for: unit) - let glucoseSamples = self.getGlucoseSample(unit: unit) + let glucoseSamples = await self.getGlucoseSample(unit: unit) guard let currentGlucose = glucoseSamples.last else { print("ERROR: No glucose sample found...") return @@ -118,7 +118,7 @@ class LiveActivityManager : LiveActivityManagerProxy { delta = "\(deltaValue < 0 ? "-" : "+")\(glucoseFormatter.string(from: abs(deltaValue)) ?? "??")" } - let bottomRow = self.getBottomRow( + let bottomRow = await self.getBottomRow( currentGlucose: current, delta: delta, statusContext: statusContext, @@ -308,53 +308,43 @@ class LiveActivityManager : LiveActivityManagerProxy { } } - private func getInsulinOnBoard() -> String { - let updateGroup = DispatchGroup() - var iob = "??" - - updateGroup.enter() - self.doseStore.insulinOnBoard(at: Date.now) { result in - switch (result) { - case .failure: - break - case .success(let iobValue): - iob = self.iobFormatter.string(from: iobValue.value) ?? "??" - break + private func getInsulinOnBoard() async -> String { + // NOTE: Do NOT bridge async→sync with DispatchGroup.wait(.distantFuture) here. + // update() runs on a Swift Concurrency cooperative-pool thread; blocking that + // thread starves the (core-count-sized) pool and can deadlock the whole + // concurrency runtime under repeated Live Activity updates. Suspend instead. + return await withCheckedContinuation { continuation in + self.doseStore.insulinOnBoard(at: Date.now) { result in + switch (result) { + case .failure: + continuation.resume(returning: "??") + case .success(let iobValue): + continuation.resume(returning: self.iobFormatter.string(from: iobValue.value) ?? "??") + } } - - updateGroup.leave() } - - _ = updateGroup.wait(timeout: .distantFuture) - return iob } - - private func getGlucoseSample(unit: HKUnit) -> [StoredGlucoseSample] { - let updateGroup = DispatchGroup() - var samples: [StoredGlucoseSample] = [] - - updateGroup.enter() - + + private func getGlucoseSample(unit: HKUnit) async -> [StoredGlucoseSample] { // When in spacious mode, we want to show the predictive line // In compact mode, we only want to show the history let timeInterval: TimeInterval = self.settings.addPredictiveLine ? .hours(-2) : .hours(-6) - self.glucoseStore.getGlucoseSamples( - start: adjustedChartStart(Date.now.addingTimeInterval(timeInterval)), - end: Date.now - ) { result in - switch (result) { - case .failure: - break - case .success(let data): - samples = data - break + + // NOTE: See getInsulinOnBoard() — never block the cooperative pool with a + // DispatchGroup.wait here; suspend the async task instead. + return await withCheckedContinuation { continuation in + self.glucoseStore.getGlucoseSamples( + start: adjustedChartStart(Date.now.addingTimeInterval(timeInterval)), + end: Date.now + ) { result in + switch (result) { + case .failure: + continuation.resume(returning: []) + case .success(let data): + continuation.resume(returning: data) + } } - - updateGroup.leave() } - - _ = updateGroup.wait(timeout: .distantFuture) - return samples } // If the chart start falls past the half-hour mark (HH:31–HH:59), pull it back to HH:30 @@ -490,43 +480,45 @@ class LiveActivityManager : LiveActivityManagerProxy { return glucoseRanges } - private func getBottomRow(currentGlucose: Double, delta: String, statusContext: StatusExtensionContext?, glucoseFormatter: NumberFormatter) -> [BottomRowItem] { - return self.settings.bottomRowConfiguration.map { type in + private func getBottomRow(currentGlucose: Double, delta: String, statusContext: StatusExtensionContext?, glucoseFormatter: NumberFormatter) async -> [BottomRowItem] { + var items: [BottomRowItem] = [] + for type in self.settings.bottomRowConfiguration { switch(type) { case .iob: - return BottomRowItem.generic(label: type.name(), value: getInsulinOnBoard(), unit: "U") - + items.append(BottomRowItem.generic(label: type.name(), value: await getInsulinOnBoard(), unit: "U")) + case .cob: var cob: String = "0" if let cobValue = statusContext?.carbsOnBoard { cob = self.cobFormatter.string(from: cobValue) ?? "??" } - return BottomRowItem.generic(label: type.name(), value: cob, unit: "g") - + items.append(BottomRowItem.generic(label: type.name(), value: cob, unit: "g")) + case .basal: guard let netBasalContext = statusContext?.netBasal else { - return BottomRowItem.basal(rate: 0, percentage: 0) + items.append(BottomRowItem.basal(rate: 0, percentage: 0)) + continue } + items.append(BottomRowItem.basal(rate: netBasalContext.rate, percentage: netBasalContext.percentage)) - return BottomRowItem.basal(rate: netBasalContext.rate, percentage: netBasalContext.percentage) - case .currentBg: - return BottomRowItem.currentBg(label: type.name(), value: "\(glucoseFormatter.string(from: currentGlucose) ?? "??")", trend: statusContext?.glucoseDisplay?.trendType) - + items.append(BottomRowItem.currentBg(label: type.name(), value: "\(glucoseFormatter.string(from: currentGlucose) ?? "??")", trend: statusContext?.glucoseDisplay?.trendType)) + case .eventualBg: guard let eventual = statusContext?.predictedGlucose?.values.last else { - return BottomRowItem.generic(label: type.name(), value: "??", unit: "") + items.append(BottomRowItem.generic(label: type.name(), value: "??", unit: "")) + continue } - - return BottomRowItem.generic(label: type.name(), value: glucoseFormatter.string(from: eventual) ?? "??", unit: "") - + items.append(BottomRowItem.generic(label: type.name(), value: glucoseFormatter.string(from: eventual) ?? "??", unit: "")) + case .deltaBg: - return BottomRowItem.generic(label: type.name(), value: delta, unit: "") - + items.append(BottomRowItem.generic(label: type.name(), value: delta, unit: "")) + case .updatedAt: - return BottomRowItem.generic(label: type.name(), value: timeFormatter.string(from: Date.now), unit: "") + items.append(BottomRowItem.generic(label: type.name(), value: timeFormatter.string(from: Date.now), unit: "")) } - } + } + return items } private func initEmptyActivity(settings: LiveActivitySettings) { From 3d21cdc40fc40a02dea0b0b91db124d621d4994a Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 25 Jul 2026 14:48:34 -0500 Subject: [PATCH 03/26] Wire pump BLE heartbeat to the CGM reading schedule Send the pump a PumpHeartbeatRequest (last CGM reading date + expected reading interval) when it must provide the BLE heartbeat, or nil when the CGM wakes the app itself, via the setBLEHeartbeatRequest API. The last-reading time is refreshed after every CGM reading (processCGMReadingResult already calls updatePumpManagerBLEHeartbeatPreference), so the pump's heartbeat cadence tracks the actual reading schedule. Requires the LoopKit setBLEHeartbeatRequest API on dev (LoopKit dev backport). --- Loop/Managers/DeviceDataManager.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index ff678a8824..3d07128ebb 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -930,7 +930,17 @@ extension DeviceDataManager { } func updatePumpManagerBLEHeartbeatPreference() { - pumpManager?.setMustProvideBLEHeartbeat(pumpManagerMustProvideBLEHeartbeat) + guard pumpManagerMustProvideBLEHeartbeat else { + pumpManager?.setBLEHeartbeatRequest(nil) + return + } + // Tell the pump when the last CGM reading landed and how often readings are expected, so it can + // schedule its next heartbeat to arrive just after the next reading is due (the pump adds its own + // buffer for the remote CGM value to be fetched and stored). + let request = PumpHeartbeatRequest( + lastCGMReadingDate: glucoseStore.latestGlucose?.startDate, + expectedCGMReadingInterval: cgmManager?.expectedGlucoseSampleInterval ?? .minutes(5)) + pumpManager?.setBLEHeartbeatRequest(request) } } From 3894e1ab67067798b43d9bf2f37c0415294f412f Mon Sep 17 00:00:00 2001 From: marionbarker Date: Tue, 28 Jul 2026 14:27:26 -0700 Subject: [PATCH 04/26] bump WATCHOS_DEPLOYMENT_TARGET to 9.0 to match LoopKit --- Loop.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 005d0e00be..624fe51e6b 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -4300,7 +4300,7 @@ TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; WARNING_CFLAGS = "-Wall"; - WATCHOS_DEPLOYMENT_TARGET = 7.1; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Debug; }; @@ -4412,7 +4412,7 @@ VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; WARNING_CFLAGS = "-Wall"; - WATCHOS_DEPLOYMENT_TARGET = 7.1; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Release; }; From 0b10e398eba2b9d9ca50ce2d75941014e8cbc670 Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Thu, 13 Aug 2026 11:10:00 -0700 Subject: [PATCH 05/26] Enable Liquid Glass + Fix Separated Toolbar + Liquid Glass Fixes (#2466) * toolbar fix * toolbar fix * toolbar fix * Liquid Glass Bug Fixes (Pt. 1) * Fix settings popup after dismissal * Fix settings popup after dismissal --- Loop/Extensions/UIAlertController.swift | 14 +- Loop/Info.plist | 2 - Loop/Managers/AlertPermissionsChecker.swift | 2 +- .../StatusTableViewController.swift | 153 ++++++++++++------ Loop/Views/SettingsView.swift | 147 ++++++++++++----- 5 files changed, 225 insertions(+), 93 deletions(-) diff --git a/Loop/Extensions/UIAlertController.swift b/Loop/Extensions/UIAlertController.swift index 3b83aa11d8..dbec0093fc 100644 --- a/Loop/Extensions/UIAlertController.swift +++ b/Loop/Extensions/UIAlertController.swift @@ -88,6 +88,8 @@ extension UIAlertController { } )) } + + addCancelAction() } /// Initializes an action sheet-styled controller for selecting a CGMManager @@ -112,6 +114,8 @@ extension UIAlertController { } )) } + + addCancelAction() } internal convenience init(deleteCGMManagerHandler handler: @escaping (_ isDeleted: Bool) -> Void) { @@ -141,10 +145,12 @@ extension UIAlertController { /// - selectionHandler: A closure to execute when a service is selected. /// - identifier: The identifier of the selected service. internal convenience init(availableServices: [ServiceDescriptor], selectionHandler: @escaping (_ identifier: String) -> Void) { + let preferredStyle: UIAlertController.Style = .alert + self.init( title: NSLocalizedString("Add Service", comment: "Action sheet title selecting service"), message: nil, - preferredStyle: .actionSheet + preferredStyle: preferredStyle ) for availableService in availableServices { @@ -156,11 +162,15 @@ extension UIAlertController { } )) } + + if #available(iOS 26.0, *) { + addCancelAction() + } } internal func addCancelAction(handler: ((UIAlertAction) -> Void)? = nil) { let cancel = NSLocalizedString("Cancel", comment: "The title of the cancel action in an action sheet") - addAction(UIAlertAction(title: cancel, style: .cancel, handler: handler)) + addAction(UIAlertAction(title: cancel, style: .destructive, handler: handler)) } } diff --git a/Loop/Info.plist b/Loop/Info.plist index db76e6e846..d6ee292459 100644 --- a/Loop/Info.plist +++ b/Loop/Info.plist @@ -89,8 +89,6 @@ remote-notification audio - UIDesignRequiresCompatibility - UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/Loop/Managers/AlertPermissionsChecker.swift b/Loop/Managers/AlertPermissionsChecker.swift index bae4512e6a..fcd5edcbff 100644 --- a/Loop/Managers/AlertPermissionsChecker.swift +++ b/Loop/Managers/AlertPermissionsChecker.swift @@ -128,7 +128,7 @@ extension AlertPermissionsChecker { message: Self.unsafeNotificationPermissionsAlertContent.body, preferredStyle: .alert) let titleImageAttachment = NSTextAttachment() - titleImageAttachment.image = UIImage(systemName: "exclamationmark.triangle.fill")?.withTintColor(.critical) + titleImageAttachment.image = UIImage(systemName: "exclamationmark.triangle.fill")?.withTintColor(.critical, renderingMode: .alwaysOriginal) titleImageAttachment.bounds = CGRect(x: titleImageAttachment.bounds.origin.x, y: -10, width: 40, height: 35) let titleWithImage = NSMutableAttributedString(attachment: titleImageAttachment) titleWithImage.append(NSMutableAttributedString(string: "\n\n", attributes: [.font: UIFont.systemFont(ofSize: 8)])) diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 94df013244..37751e093b 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -273,41 +273,76 @@ final class StatusTableViewController: LoopChartsTableViewController { deviceManager.pumpManagerHUDProvider?.visible = active && onscreen } + private lazy var carbEntryButton = makeToolbarButton(imageNamed: "carbs", tintColor: .carbTintColor, action: #selector(userTappedAddCarbs)) + private lazy var bolusButton = makeToolbarButton(imageNamed: "bolus", tintColor: .insulinTintColor, action: #selector(presentBolusScreen)) + private lazy var settingsButton = makeToolbarButton(imageNamed: "settings", tintColor: .secondaryLabel, action: #selector(onSettingsTapped)) + + private lazy var workoutButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .glucoseTintColor + button.addTarget(self, action: #selector(toggleWorkoutMode(_:)), for: .touchUpInside) + button.constrainToToolbarIconSize() + return button + }() + + private func makeToolbarButton(imageNamed name: String, tintColor: UIColor, action: Selector) -> UIButton { + let button = UIButton(type: .system) + button.setImage(UIImage(named: name), for: .normal) + button.tintColor = tintColor + button.addTarget(self, action: action, for: .touchUpInside) + button.constrainToToolbarIconSize() + return button + } + private func setupToolbarItems() { - let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil) - let carbs = UIBarButtonItem(image: UIImage(named: "carbs"), style: .plain, target: self, action: #selector(userTappedAddCarbs)) - let bolus = UIBarButtonItem(image: UIImage(named: "bolus"), style: .plain, target: self, action: #selector(presentBolusScreen)) - let settings = UIBarButtonItem(image: UIImage(named: "settings"), style: .plain, target: self, action: #selector(onSettingsTapped)) - + let carbs = UIBarButtonItem(customView: carbEntryButton) + let bolus = UIBarButtonItem(customView: bolusButton) + let settings = UIBarButtonItem(customView: settingsButton) + + carbs.title = "Add Meal" + let preMeal = createPreMealButtonItem(selected: false, isEnabled: true) - let workout = createWorkoutButtonItem(selected: false, isEnabled: true) - toolbarItems = [ - carbs, - space, - preMeal, - space, - bolus, - space, - workout, - space, - settings - ] - } + updateWorkoutButton(selected: false, isEnabled: true) + let workout = UIBarButtonItem(customView: workoutButton) + + func flexibleSpace() -> UIBarButtonItem { + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + } + if #available(iOS 26, *) { + toolbarItems = [carbs, preMeal, bolus, workout, settings] + } else { + toolbarItems = [ + carbs, + flexibleSpace(), + preMeal, + flexibleSpace(), + bolus, + flexibleSpace(), + workout, + flexibleSpace(), + settings + ] + } + } + private func updateToolbarItems() { let isPumpOnboarded = onboardingManager.isComplete || deviceManager.pumpManager?.isOnboarded == true - toolbarItems![0].accessibilityLabel = NSLocalizedString("Add Meal", comment: "The label of the carb entry button") - toolbarItems![0].isEnabled = isPumpOnboarded - toolbarItems![0].tintColor = UIColor.carbTintColor - toolbarItems![4].accessibilityLabel = NSLocalizedString("Bolus", comment: "The label of the bolus entry button") - toolbarItems![4].isEnabled = isPumpOnboarded - toolbarItems![4].tintColor = UIColor.insulinTintColor - toolbarItems![8].accessibilityLabel = NSLocalizedString("Settings", comment: "The label of the settings button") - toolbarItems![8].tintColor = UIColor.secondaryLabel - - toolbarItems![2] = createPreMealButtonItem(selected: preMealMode == true && preMealModeAllowed, isEnabled: preMealModeAllowed) - toolbarItems![6] = createWorkoutButtonItem(selected: workoutMode == true && workoutModeAllowed, isEnabled: workoutModeAllowed) + carbEntryButton.accessibilityLabel = NSLocalizedString("Add Meal", comment: "The label of the carb entry button") + carbEntryButton.isEnabled = isPumpOnboarded + bolusButton.accessibilityLabel = NSLocalizedString("Bolus", comment: "The label of the bolus entry button") + bolusButton.isEnabled = isPumpOnboarded + settingsButton.accessibilityLabel = NSLocalizedString("Settings", comment: "The label of the settings button") + + let preMealIndex: Int + if #available(iOS 26, *) { + preMealIndex = 1 + } else { + preMealIndex = 2 + } + toolbarItems![preMealIndex] = createPreMealButtonItem(selected: preMealMode == true && preMealModeAllowed, isEnabled: preMealModeAllowed) + updateWorkoutButton(selected: workoutMode == true && workoutModeAllowed, isEnabled: workoutModeAllowed) } public var basalDeliveryState: PumpManagerStatus.BasalDeliveryState? = nil { @@ -1378,7 +1413,7 @@ final class StatusTableViewController: LoopChartsTableViewController { let bolusEntryView = SimpleBolusView(viewModel: viewModel).environmentObject(deviceManager.displayGlucosePreference) let hostingController = DismissibleHostingController(rootView: bolusEntryView, isModalInPresentation: false) navigationWrapper = UINavigationController(rootViewController: hostingController) - hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: navigationWrapper, action: #selector(dismissWithAnimation)) + hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Cancel", comment: ""), style: .plain, target: navigationWrapper, action: #selector(dismissWithAnimation)) present(navigationWrapper, animated: true) } else { let viewModel = CarbEntryViewModel(delegate: deviceManager) @@ -1437,7 +1472,7 @@ final class StatusTableViewController: LoopChartsTableViewController { ) let navigationWrapper = UINavigationController(rootViewController: hostingController) - hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: navigationWrapper, action: #selector(dismissWithAnimation)) + hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Cancel", comment: ""), style: .plain, target: navigationWrapper, action: #selector(dismissWithAnimation)) present(navigationWrapper, animated: true) deviceManager.analyticsServicesManager.didDisplayBolusScreen() } @@ -1459,21 +1494,20 @@ final class StatusTableViewController: LoopChartsTableViewController { return item } - private func createWorkoutButtonItem(selected: Bool, isEnabled: Bool) -> UIBarButtonItem { - let item = UIBarButtonItem(image: UIImage.workoutImage(selected: selected), style: .plain, target: self, action: #selector(toggleWorkoutMode(_:))) - item.accessibilityLabel = NSLocalizedString("Workout Targets", comment: "The label of the workout mode toggle button") + private func updateWorkoutButton(selected: Bool, isEnabled: Bool) { + workoutButton.setImage(UIImage.workoutImage(selected: selected), for: .normal) + workoutButton.accessibilityLabel = NSLocalizedString("Workout Targets", comment: "The label of the workout mode toggle button") if selected { - item.accessibilityTraits.insert(.selected) - item.accessibilityHint = NSLocalizedString("Disables", comment: "The action hint of the workout mode toggle button when enabled") + workoutButton.accessibilityTraits.insert(.selected) + workoutButton.accessibilityHint = NSLocalizedString("Disables", comment: "The action hint of the workout mode toggle button when enabled") } else { - item.accessibilityHint = NSLocalizedString("Enables", comment: "The action hint of the workout mode toggle button when disabled") + workoutButton.accessibilityTraits.remove(.selected) + workoutButton.accessibilityHint = NSLocalizedString("Enables", comment: "The action hint of the workout mode toggle button when disabled") } - item.tintColor = UIColor.glucoseTintColor - item.isEnabled = isEnabled - - return item + workoutButton.isEnabled = isEnabled + workoutButton.sizeToFit() } @IBAction func premealButtonTapped(_ sender: UIBarButtonItem) { @@ -1544,7 +1578,13 @@ final class StatusTableViewController: LoopChartsTableViewController { } } else { if FeatureFlags.sensitivityOverridesEnabled { - performSegue(withIdentifier: OverrideSelectionViewController.className, sender: toolbarItems![6]) + let overridesIndex: Int + if #available(iOS 26, *) { + overridesIndex = 3 + } else { + overridesIndex = 6 + } + performSegue(withIdentifier: OverrideSelectionViewController.className, sender: toolbarItems![overridesIndex]) } else { presentWorkoutModeAlertController() } @@ -1781,15 +1821,15 @@ final class StatusTableViewController: LoopChartsTableViewController { @objc private func pumpStatusTapped(_ sender: UIGestureRecognizer) { if let pumpStatusView = sender.view as? PumpStatusHUDView { - executeHUDTapAction(deviceManager.didTapOnPumpStatus(pumpStatusView.pumpManagerProvidedHUD)) + executeHUDTapAction(deviceManager.didTapOnPumpStatus(pumpStatusView.pumpManagerProvidedHUD), from: sender.view) } } @objc private func cgmStatusTapped( _ sender: UIGestureRecognizer) { - executeHUDTapAction(deviceManager.didTapOnCGMStatus()) + executeHUDTapAction(deviceManager.didTapOnCGMStatus(), from: sender.view) } - private func executeHUDTapAction(_ action: HUDTapAction?) { + private func executeHUDTapAction(_ action: HUDTapAction?, from sourceView: UIView?) { guard let action = action else { return } @@ -1802,15 +1842,15 @@ final class StatusTableViewController: LoopChartsTableViewController { case .openAppURL(let url): UIApplication.shared.open(url) case .setupNewCGM: - addNewCGMManager() + addNewCGMManager(from: sourceView) case .setupNewPump: - addNewPumpManager() + addNewPumpManager(from: sourceView) default: return } } - private func addNewPumpManager() { + private func addNewPumpManager(from sourceView: UIView?) { let availablePumpManagers = deviceManager.availablePumpManagers switch availablePumpManagers.count { @@ -1822,12 +1862,13 @@ final class StatusTableViewController: LoopChartsTableViewController { let alert = UIAlertController(availablePumpManagers: availablePumpManagers) { [weak self] (identifier) in self?.addPumpManager(withIdentifier: identifier) } - alert.addCancelAction { _ in } + alert.popoverPresentationController?.sourceView = sourceView ?? view + alert.popoverPresentationController?.sourceRect = sourceView?.bounds ?? view.bounds present(alert, animated: true, completion: nil) } } - private func addNewCGMManager() { + private func addNewCGMManager(from sourceView: UIView?) { let availableCGMManagers = deviceManager.availableCGMManagers switch availableCGMManagers.count { @@ -1839,7 +1880,8 @@ final class StatusTableViewController: LoopChartsTableViewController { let alert = UIAlertController(availableCGMManagers: availableCGMManagers) { [weak self] identifier in self?.addCGMManager(withIdentifier: identifier) } - alert.addCancelAction { _ in } + alert.popoverPresentationController?.sourceView = sourceView ?? view + alert.popoverPresentationController?.sourceRect = sourceView?.bounds ?? view.bounds present(alert, animated: true, completion: nil) } } @@ -2042,6 +2084,15 @@ final class StatusTableViewController: LoopChartsTableViewController { } } +private extension UIButton { + func constrainToToolbarIconSize() { + setContentHuggingPriority(.required, for: .horizontal) + setContentHuggingPriority(.required, for: .vertical) + setContentCompressionResistancePriority(.required, for: .horizontal) + setContentCompressionResistancePriority(.required, for: .vertical) + } +} + extension UIAlertController { func addActivityIndicator() { let frame = CGRect(x: 0, y: 0, width: 40, height: 40) diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index aa0da33134..4d65f5b2ca 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -107,25 +107,6 @@ public struct SettingsView: View { .insetGroupedListStyle() .navigationBarTitle(Text(NSLocalizedString("Settings", comment: "Settings screen title"))) .navigationBarItems(trailing: dismissButton) - .actionSheet(item: $actionSheet) { actionSheet in - switch actionSheet { - case .cgmPicker: - return ActionSheet( - title: Text("Add CGM", comment: "The title of the CGM chooser in settings"), - buttons: cgmChoices - ) - case .pumpPicker: - return ActionSheet( - title: Text("Add Pump", comment: "The title of the pump chooser in settings"), - buttons: pumpChoices - ) - case .servicePicker: - return ActionSheet( - title: Text("Add Service", comment: "The title of the add service action sheet in settings"), - buttons: serviceChoices - ) - } - } .alert(item: $alert) { alert in switch alert { case .deleteCGMData: @@ -337,17 +318,22 @@ extension SettingsView { imageView: plusImage, label: NSLocalizedString("Add Pump", comment: "Title text for button to add pump device"), descriptiveText: NSLocalizedString("Tap here to set up a pump", comment: "Descriptive text for button to add pump device")) + .background( + PluginPopover( + isPresented: actionSheetBinding(for: .pumpPicker), + title: NSLocalizedString("Add Pump", comment: "The title of the pump chooser in settings"), + actions: pumpChoices + ) + ) } } - - private var pumpChoices: [ActionSheet.Button] { - var result = viewModel.pumpManagerSettingsViewModel.availableDevices.map { availableDevice in - ActionSheet.Button.default(Text(availableDevice.localizedTitle)) { + + private var pumpChoices: [PluginPopover.Action] { + viewModel.pumpManagerSettingsViewModel.availableDevices.map { availableDevice in + .init(title: availableDevice.localizedTitle) { self.viewModel.pumpManagerSettingsViewModel.didTapAdd(availableDevice) } } - result.append(.cancel()) - return result } @ViewBuilder @@ -364,6 +350,13 @@ extension SettingsView { imageView: plusImage, label: NSLocalizedString("Add CGM", comment: "Title text for button to add CGM device"), descriptiveText: NSLocalizedString("Tap here to set up a CGM", comment: "Descriptive text for button to add CGM device")) + .background( + PluginPopover( + isPresented: actionSheetBinding(for: .cgmPicker), + title: NSLocalizedString("Add CGM", comment: "The title of the CGM chooser in settings"), + actions: cgmChoices + ) + ) } } @@ -377,16 +370,14 @@ extension SettingsView { } } - private var cgmChoices: [ActionSheet.Button] { - var result = viewModel.cgmManagerSettingsViewModel.availableDevices + private var cgmChoices: [PluginPopover.Action] { + viewModel.cgmManagerSettingsViewModel.availableDevices .sorted(by: {$0.localizedTitle < $1.localizedTitle}) .map { availableDevice in - ActionSheet.Button.default(Text(availableDevice.localizedTitle)) { + .init(title: availableDevice.localizedTitle) { self.viewModel.cgmManagerSettingsViewModel.didTapAdd(availableDevice) + } } - } - result.append(.cancel()) - return result } private var servicesSection: some View { @@ -404,20 +395,36 @@ extension SettingsView { imageView: plusImage, label: NSLocalizedString("Add Service", comment: "The title of the add service button in settings"), descriptiveText: NSLocalizedString("Tap here to set up a Service", comment: "The descriptive text of the add service button in settings")) + .background( + PluginPopover( + isPresented: actionSheetBinding(for: .servicePicker), + title: NSLocalizedString("Add Service", comment: "The title of the add service action sheet in settings"), + actions: serviceChoices + ) + ) } } } - - private var serviceChoices: [ActionSheet.Button] { - var result = viewModel.servicesViewModel.inactiveServices().map { availableService in - ActionSheet.Button.default(Text(availableService.localizedTitle)) { + + private var serviceChoices: [PluginPopover.Action] { + viewModel.servicesViewModel.inactiveServices().map { availableService in + .init(title: availableService.localizedTitle) { self.viewModel.servicesViewModel.didTapAddService(availableService) } } - result.append(.cancel()) - return result } + private func actionSheetBinding(for destination: Destination.ActionSheet) -> Binding { + Binding( + get: { actionSheet == destination }, + set: { isPresented in + if !isPresented && actionSheet == destination { + actionSheet = nil + } + } + ) + } + private var deleteDataSection: some View { Section { if viewModel.pumpManagerSettingsViewModel.isTestingDevice { @@ -615,6 +622,72 @@ fileprivate struct LargeButton: View { } } +struct PluginPopover: UIViewControllerRepresentable { + struct Action { + let title: String + let handler: () -> Void + } + + @Binding var isPresented: Bool + let title: String + let actions: [Action] + + func makeCoordinator() -> Coordinator { + Coordinator() + } + + func makeUIViewController(context: Context) -> UIViewController { + UIViewController() + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + let coordinator = context.coordinator + + if !isPresented { + coordinator.didPresent = false + return + } + + guard !coordinator.didPresent else { return } + guard uiViewController.presentedViewController == nil else { return } + + coordinator.didPresent = true + coordinator.onDismiss = { self.isPresented = false } + + let alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) + for action in actions { + alert.addAction(UIAlertAction(title: action.title, style: .default) { _ in + self.isPresented = false + action.handler() + }) + } + + alert.addAction(UIAlertAction( + title: NSLocalizedString("Cancel", comment: "The title of the cancel action in an action sheet"), + style: .destructive + ) { _ in + self.isPresented = false + }) + + if let popover = alert.popoverPresentationController { + popover.sourceView = uiViewController.view + popover.sourceRect = uiViewController.view.bounds + popover.delegate = coordinator + } + + uiViewController.present(alert, animated: true) + } + + final class Coordinator: NSObject, UIPopoverPresentationControllerDelegate { + var didPresent = false + var onDismiss: (() -> Void)? + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + onDismiss?() + } + } +} + public struct SettingsView_Previews: PreviewProvider { public static var previews: some View { From 715b7ca788f6597d0e29a591b4dd503f1f50f519 Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Thu, 13 Aug 2026 11:10:27 -0700 Subject: [PATCH 06/26] Xcode 27 Build Support (#2458) * xcode 27 building * xcode 27 building * xcode 27 building * xcode 27 building * Rename for better understanding * Rename for better understanding --- Loop.xcodeproj/project.pbxproj | 10 ++ .../xcshareddata/xcschemes/WatchApp.xcscheme | 2 - Loop/AppDelegate.swift | 59 +++------- Loop/Info.plist | 21 +++- Loop/Managers/LoopAppManager.swift | 2 + Loop/SceneDelegate.swift | 101 ++++++++++++++++++ Loop/Views/FavoriteFoodDetailView.swift | 2 +- Scripts/capture-build-details.sh | 6 +- 8 files changed, 154 insertions(+), 49 deletions(-) create mode 100644 Loop/SceneDelegate.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 624fe51e6b..dfcb0b9516 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -258,6 +258,7 @@ 84AA81E32A4A36FB000B658B /* SystemActionLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA81E22A4A36FB000B658B /* SystemActionLink.swift */; }; 84AA81E52A4A3981000B658B /* DeeplinkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA81E42A4A3981000B658B /* DeeplinkManager.swift */; }; 84AA81E72A4A4DEF000B658B /* PumpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA81E62A4A4DEF000B658B /* PumpView.swift */; }; + 84AB0A833008A62400A13446 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AB0A823008A62400A13446 /* SceneDelegate.swift */; }; 84D2879F2AC756C8007ED283 /* ContentMargin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D2879E2AC756C8007ED283 /* ContentMargin.swift */; }; 891B508524342BE1005DA578 /* CarbAndBolusFlowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 891B508424342BE1005DA578 /* CarbAndBolusFlowViewModel.swift */; }; 892A5D59222F0A27008961AB /* Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892A5D58222F0A27008961AB /* Debug.swift */; }; @@ -1065,6 +1066,7 @@ 84AA81E22A4A36FB000B658B /* SystemActionLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemActionLink.swift; sourceTree = ""; }; 84AA81E42A4A3981000B658B /* DeeplinkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeeplinkManager.swift; sourceTree = ""; }; 84AA81E62A4A4DEF000B658B /* PumpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpView.swift; sourceTree = ""; }; + 84AB0A823008A62400A13446 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 84D2879E2AC756C8007ED283 /* ContentMargin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentMargin.swift; sourceTree = ""; }; 891B508424342BE1005DA578 /* CarbAndBolusFlowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbAndBolusFlowViewModel.swift; sourceTree = ""; }; 892A5D29222EF60A008961AB /* MockKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; name = MockKit.framework; path = Carthage/Build/iOS/MockKit.framework; sourceTree = SOURCE_ROOT; }; @@ -1745,6 +1747,7 @@ 43F5C2CE1B92A2A0003EB13D /* View Controllers */, 43F5C2CF1B92A2ED003EB13D /* Views */, 897A5A9724C22DCE00C4E71D /* View Models */, + 84AB0A823008A62400A13446 /* SceneDelegate.swift */, ); path = Loop; sourceTree = ""; @@ -3523,6 +3526,7 @@ A9DCF32A25B0FABF00C89088 /* LoopUIColorPalette+Default.swift in Sources */, 1DDE273E24AEA4B000796622 /* SettingsView.swift in Sources */, A9B607B0247F000F00792BE4 /* UserNotifications+Loop.swift in Sources */, + 84AB0A833008A62400A13446 /* SceneDelegate.swift in Sources */, 43F89CA322BDFBBD006BB54E /* UIActivityIndicatorView.swift in Sources */, A999D40624663D18004C89D4 /* PumpManagerError.swift in Sources */, 437D9BA31D7BC977007245E8 /* PredictionTableViewController.swift in Sources */, @@ -4497,6 +4501,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "WatchApp Extension/Extensions/WatchApp Extension-Bridging-Header.h"; SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Debug; }; @@ -4524,6 +4529,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "WatchApp Extension/Extensions/WatchApp Extension-Bridging-Header.h"; SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Release; }; @@ -4548,6 +4554,7 @@ SDKROOT = watchos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Debug; }; @@ -4572,6 +4579,7 @@ SDKROOT = watchos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Release; }; @@ -4598,6 +4606,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INSTALL_OBJC_HEADER = NO; TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Debug; }; @@ -4624,6 +4633,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INSTALL_OBJC_HEADER = NO; TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Release; }; diff --git a/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme b/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme index 6ab6be0246..e2a1660877 100644 --- a/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme +++ b/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme @@ -94,7 +94,6 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "43A943711B926B7B0051FA24" BuildableName = "WatchApp.app" - BlueprintName = "WatchApp" ReferencedContainer = "container:Loop.xcodeproj"> @@ -112,7 +111,6 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "43A943711B926B7B0051FA24" BuildableName = "WatchApp.app" - BlueprintName = "WatchApp" ReferencedContainer = "container:Loop.xcodeproj"> diff --git a/Loop/AppDelegate.swift b/Loop/AppDelegate.swift index ebb05d5c12..f54306c3eb 100644 --- a/Loop/AppDelegate.swift +++ b/Loop/AppDelegate.swift @@ -9,10 +9,16 @@ import UIKit import LoopKit -final class AppDelegate: UIResponder, UIApplicationDelegate, WindowProvider { - var window: UIWindow? +final class AppDelegate: UIResponder, UIApplicationDelegate { + + /// The shared app manager. Per-scene lifecycle is driven by `SceneDelegate`, which + /// accesses this instance through the app delegate. + let loopAppManager = LoopAppManager() + + /// The launch options passed at `didFinishLaunchingWithOptions`, retained so the + /// scene delegate can forward them to `LoopAppManager` when it connects. + private(set) var launchOptions: [UIApplication.LaunchOptionsKey: Any]? - private let loopAppManager = LoopAppManager() private let log = DiagnosticLog(category: "AppDelegate") // MARK: - UIApplicationDelegate - Initialization @@ -24,35 +30,16 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, WindowProvider { log.default("lastPathComponent = %{public}@", String(describing: Bundle.main.appStoreReceiptURL?.lastPathComponent)) - loopAppManager.initialize(windowProvider: self, launchOptions: launchOptions) - loopAppManager.launch() - return loopAppManager.isLaunchComplete - } - - // MARK: - UIApplicationDelegate - Life Cycle - - func applicationDidBecomeActive(_ application: UIApplication) { - log.default(#function) - - loopAppManager.didBecomeActive() - } - - func applicationWillResignActive(_ application: UIApplication) { - log.default(#function) - } - - func applicationDidEnterBackground(_ application: UIApplication) { - log.default(#function) + // The window is owned by the scene, so app manager initialization and launch are + // deferred to `SceneDelegate.scene(_:willConnectTo:options:)`. + self.launchOptions = launchOptions + return true } - func applicationWillEnterForeground(_ application: UIApplication) { - log.default(#function) - - loopAppManager.askUserToConfirmLoopReset() - } + // MARK: - UIApplicationDelegate - Scene Configuration - func applicationWillTerminate(_ application: UIApplication) { - log.default(#function) + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } // MARK: - UIApplicationDelegate - Environment @@ -83,20 +70,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, WindowProvider { completionHandler(loopAppManager.handleRemoteNotification(userInfo as? [String: AnyObject]) ? .noData : .failed) } - - // MARK: - UIApplicationDelegate - Deeplinking - - func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { - loopAppManager.handle(url) - } - - // MARK: - UIApplicationDelegate - Continuity - - func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { - log.default(#function) - - return loopAppManager.userActivity(userActivity, restorationHandler: restorationHandler) - } // MARK: - UIApplicationDelegate - Interface diff --git a/Loop/Info.plist b/Loop/Info.plist index d6ee292459..0acbff0673 100644 --- a/Loop/Info.plist +++ b/Loop/Info.plist @@ -82,6 +82,25 @@ NewCarbEntryIntent ViewLoopStatus + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + UIBackgroundModes bluetooth-central @@ -91,8 +110,6 @@ UILaunchStoryboardName LaunchScreen - UIMainStoryboardFile - Main UIRequiredDeviceCapabilities armv7 diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 3f00104c5a..04d561a0d3 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -128,6 +128,8 @@ class LoopAppManager: NSObject { resumeLaunch() } + var isInInitialState: Bool { state == .initialize } + var isLaunchPending: Bool { state == .checkProtectedDataAvailable } var isLaunchComplete: Bool { state == .launchComplete } diff --git a/Loop/SceneDelegate.swift b/Loop/SceneDelegate.swift new file mode 100644 index 0000000000..eea0299a22 --- /dev/null +++ b/Loop/SceneDelegate.swift @@ -0,0 +1,101 @@ +// +// SceneDelegate.swift +// Loop +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import UIKit +import LoopKit + +final class SceneDelegate: UIResponder, UIWindowSceneDelegate, WindowProvider { + var window: UIWindow? + + private let log = DiagnosticLog(category: "SceneDelegate") + + /// The app manager lives on the app delegate because it also serves app-level callbacks + /// (remote notifications, protected data availability). + private var loopAppManager: LoopAppManager? { + (UIApplication.shared.delegate as? AppDelegate)?.loopAppManager + } + + // MARK: - UIWindowSceneDelegate - Life Cycle + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + log.default(#function) + + guard let windowScene = scene as? UIWindowScene else { return } + + // When the scene is configured with a storyboard, UIKit creates the window and its + // root view controller and assigns them here. Create one defensively otherwise. + if window == nil { + let window = UIWindow(windowScene: windowScene) + self.window = window + window.makeKeyAndVisible() + } + + guard let loopAppManager else { return } + + let launchOptions = (UIApplication.shared.delegate as? AppDelegate)?.launchOptions + if loopAppManager.isInInitialState { + loopAppManager.initialize(windowProvider: self, launchOptions: launchOptions) + } + if loopAppManager.isLaunchPending { + loopAppManager.launch() + } + + if let url = connectionOptions.urlContexts.first?.url { + _ = loopAppManager.handle(url) + } + + for userActivity in connectionOptions.userActivities { + restore(userActivity, with: loopAppManager) + } + } + + func sceneDidBecomeActive(_ scene: UIScene) { + log.default(#function) + + loopAppManager?.didBecomeActive() + } + + func sceneWillEnterForeground(_ scene: UIScene) { + log.default(#function) + + loopAppManager?.askUserToConfirmLoopReset() + } + + func sceneWillResignActive(_ scene: UIScene) { + log.default(#function) + } + + func sceneDidEnterBackground(_ scene: UIScene) { + log.default(#function) + } + + // MARK: - UIWindowSceneDelegate - Deeplinking + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let url = URLContexts.first?.url else { return } + _ = loopAppManager?.handle(url) + } + + // MARK: - UIWindowSceneDelegate - Continuity + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + log.default(#function) + + guard let loopAppManager else { return } + restore(userActivity, with: loopAppManager) + } + + // MARK: - Private + + private func restore(_ userActivity: NSUserActivity, with loopAppManager: LoopAppManager) { + // Mirror UIKit's legacy behavior of invoking `restoreUserActivityState` on each + // object the app manager hands back through the restoration handler. + _ = loopAppManager.userActivity(userActivity, restorationHandler: { objects in + objects?.forEach { $0.restoreUserActivityState(userActivity) } + }) + } +} diff --git a/Loop/Views/FavoriteFoodDetailView.swift b/Loop/Views/FavoriteFoodDetailView.swift index 8c76e1fe8f..46c4105593 100644 --- a/Loop/Views/FavoriteFoodDetailView.swift +++ b/Loop/Views/FavoriteFoodDetailView.swift @@ -14,7 +14,7 @@ public struct FavoriteFoodDetailView: View { let food: StoredFavoriteFood? let onFoodDelete: (StoredFavoriteFood) -> Void - @State private var isConfirmingDelete = false + @State private var isConfirmingDelete: Bool let carbFormatter: QuantityFormatter let absorptionTimeFormatter: DateComponentsFormatter diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index 2393e5afa4..b78eb1c592 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -126,6 +126,8 @@ fi # Gather submodule details. # We use git submodule foreach to output lines in the form: # submodule_name|branch_or_tag|commit_sha +# Tolerate failures here (e.g. a stale gitlink in the index that is no longer +# mapped in .gitmodules) so that gathering build metadata never fails the build. submodules_info=$(git submodule foreach --quiet ' sub_git_branch=$(git symbolic-ref --short -q HEAD || echo "") sub_git_tag=$(git describe --tags --exact-match 2>/dev/null || echo "") @@ -135,10 +137,12 @@ submodules_info=$(git submodule foreach --quiet ' sub_git_branch_or_tag="detached" fi echo "$name|$sub_git_branch_or_tag|$sub_git_commit_sha" -') +' 2>/dev/null) || warn "Unable to fully enumerate submodules for build details" # For each line, add a dictionary entry for that submodule. echo "${submodules_info}" | while IFS="|" read -r submodule_name sub_branch sub_sha; do + # Skip empty lines (e.g. when submodule enumeration produced no output) + [ -z "${submodule_name}" ] && continue # Create a dictionary for this submodule /usr/libexec/PlistBuddy -c "Add :${submodules_key}:${submodule_name} dict" "${info_plist_path}" /usr/libexec/PlistBuddy -c "Add :${submodules_key}:${submodule_name}:branch string ${sub_branch}" "${info_plist_path}" From 6c063085204a32abd39943415e9b37b4367a3951 Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Wed, 19 Aug 2026 18:21:15 -0700 Subject: [PATCH 07/26] Xcode 27 Fixes --- Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme | 2 -- Loop/Views/FavoriteFoodDetailView.swift | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme b/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme index 6ab6be0246..e2a1660877 100644 --- a/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme +++ b/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme @@ -94,7 +94,6 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "43A943711B926B7B0051FA24" BuildableName = "WatchApp.app" - BlueprintName = "WatchApp" ReferencedContainer = "container:Loop.xcodeproj"> @@ -112,7 +111,6 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "43A943711B926B7B0051FA24" BuildableName = "WatchApp.app" - BlueprintName = "WatchApp" ReferencedContainer = "container:Loop.xcodeproj"> diff --git a/Loop/Views/FavoriteFoodDetailView.swift b/Loop/Views/FavoriteFoodDetailView.swift index 8c76e1fe8f..125d5def98 100644 --- a/Loop/Views/FavoriteFoodDetailView.swift +++ b/Loop/Views/FavoriteFoodDetailView.swift @@ -14,7 +14,7 @@ public struct FavoriteFoodDetailView: View { let food: StoredFavoriteFood? let onFoodDelete: (StoredFavoriteFood) -> Void - @State private var isConfirmingDelete = false + @State private var isConfirmingDelete: Bool let carbFormatter: QuantityFormatter let absorptionTimeFormatter: DateComponentsFormatter @@ -23,7 +23,7 @@ public struct FavoriteFoodDetailView: View { public init(food: StoredFavoriteFood?, onFoodDelete: @escaping (StoredFavoriteFood) -> Void, isConfirmingDelete: Bool = false, carbFormatter: QuantityFormatter, absorptionTimeFormatter: DateComponentsFormatter, preferredCarbUnit: HKUnit = HKUnit.gram()) { self.food = food self.onFoodDelete = onFoodDelete - self.isConfirmingDelete = isConfirmingDelete + self.isConfirmingDelete = false self.carbFormatter = carbFormatter self.absorptionTimeFormatter = absorptionTimeFormatter self.preferredCarbUnit = preferredCarbUnit From cd190f35d17b8c27216f58d222e76a898625a444 Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Wed, 19 Aug 2026 18:30:25 -0700 Subject: [PATCH 08/26] Xcode 27 Fixes --- Loop/Views/FavoriteFoodDetailView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/Views/FavoriteFoodDetailView.swift b/Loop/Views/FavoriteFoodDetailView.swift index 125d5def98..46c4105593 100644 --- a/Loop/Views/FavoriteFoodDetailView.swift +++ b/Loop/Views/FavoriteFoodDetailView.swift @@ -23,7 +23,7 @@ public struct FavoriteFoodDetailView: View { public init(food: StoredFavoriteFood?, onFoodDelete: @escaping (StoredFavoriteFood) -> Void, isConfirmingDelete: Bool = false, carbFormatter: QuantityFormatter, absorptionTimeFormatter: DateComponentsFormatter, preferredCarbUnit: HKUnit = HKUnit.gram()) { self.food = food self.onFoodDelete = onFoodDelete - self.isConfirmingDelete = false + self.isConfirmingDelete = isConfirmingDelete self.carbFormatter = carbFormatter self.absorptionTimeFormatter = absorptionTimeFormatter self.preferredCarbUnit = preferredCarbUnit From 9fc842e19391a1a8829c986e5bf58f52fb2b2757 Mon Sep 17 00:00:00 2001 From: Marion Barker <19607791+marionbarker@users.noreply.github.com> Date: Sat, 5 Sep 2026 12:11:33 -0700 Subject: [PATCH 09/26] Updated translations from lokalise on Fri Sep 4 20:53:19 PDT 2026 (#2503) --- Common/ro.lproj/Intents.strings | 2 +- Loop Status Extension/Localizable.xcstrings | 12 + .../mul.lproj/MainInterface.xcstrings | 2 +- .../Bootstrap/InfoPlist.xcstrings | 6 + .../Bootstrap/Localizable.xcstrings | 78 ++ .../xcshareddata/xcschemes/WatchApp.xcscheme | 2 + Loop/Localizable.xcstrings | 880 +++++++++++++++++- Loop/mul.lproj/Main.xcstrings | 6 +- LoopCore/Localizable.xcstrings | 84 ++ LoopUI/InfoPlist.xcstrings | 6 + LoopUI/Localizable.xcstrings | 26 +- WatchApp Extension/Localizable.xcstrings | 54 ++ WatchApp Extension/ckcomplication.xcstrings | 24 + WatchApp/mul.lproj/Interface.xcstrings | 2 +- 14 files changed, 1130 insertions(+), 54 deletions(-) diff --git a/Common/ro.lproj/Intents.strings b/Common/ro.lproj/Intents.strings index d6634a6a46..cd9bb42c33 100644 --- a/Common/ro.lproj/Intents.strings +++ b/Common/ro.lproj/Intents.strings @@ -20,7 +20,7 @@ "OcNxIj" = "Adăugați carbohidrați"; /* (No Comment) */ -"oLQSsJ" = "Activați Suprascrierea Personalizată „$”{overrideName}"; +"oLQSsJ" = "Activați Presetarea de Suprascrierea '$'{overrideName}"; /* (No Comment) */ "XNNmtH" = "Activați presetarea în Loop"; diff --git a/Loop Status Extension/Localizable.xcstrings b/Loop Status Extension/Localizable.xcstrings index b947ae9f67..e264d1eb98 100644 --- a/Loop Status Extension/Localizable.xcstrings +++ b/Loop Status Extension/Localizable.xcstrings @@ -890,6 +890,12 @@ "state" : "translated", "value" : "dB" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "dB" + } } } }, @@ -1768,6 +1774,12 @@ "state" : "translated", "value" : "U" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "U" + } } } } diff --git a/Loop Status Extension/mul.lproj/MainInterface.xcstrings b/Loop Status Extension/mul.lproj/MainInterface.xcstrings index a75d5d94ff..bd96f6297f 100644 --- a/Loop Status Extension/mul.lproj/MainInterface.xcstrings +++ b/Loop Status Extension/mul.lproj/MainInterface.xcstrings @@ -366,7 +366,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "IOB 1.0 单位" + "value" : "IOB 1.0 U" } } } diff --git a/Loop Widget Extension/Bootstrap/InfoPlist.xcstrings b/Loop Widget Extension/Bootstrap/InfoPlist.xcstrings index aab9a9f921..864a763fa0 100644 --- a/Loop Widget Extension/Bootstrap/InfoPlist.xcstrings +++ b/Loop Widget Extension/Bootstrap/InfoPlist.xcstrings @@ -76,6 +76,12 @@ "state" : "translated", "value" : "Loop Widgets" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop 小组件" + } } } }, diff --git a/Loop Widget Extension/Bootstrap/Localizable.xcstrings b/Loop Widget Extension/Bootstrap/Localizable.xcstrings index 5bb04e12b8..09e4236b3e 100644 --- a/Loop Widget Extension/Bootstrap/Localizable.xcstrings +++ b/Loop Widget Extension/Bootstrap/Localizable.xcstrings @@ -150,6 +150,12 @@ "state" : "translated", "value" : "-Ü" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "-U" + } } } }, @@ -220,6 +226,12 @@ "state" : "translated", "value" : "??" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "??" + } } } }, @@ -454,6 +466,12 @@ "state" : "translated", "value" : "%1$@%2$@" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%2$@" + } } } }, @@ -476,6 +494,12 @@ "state" : "translated", "value" : "%@U" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@U" + } } } }, @@ -629,6 +653,12 @@ "state" : "translated", "value" : "Culoare" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "颜色" + } } } }, @@ -657,6 +687,12 @@ "state" : "translated", "value" : "Dată" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "日期" + } } } }, @@ -776,6 +812,12 @@ "state" : "translated", "value" : "dB" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "dB" + } } } }, @@ -804,6 +846,12 @@ "state" : "translated", "value" : "Sfârșit" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "结束" + } } } }, @@ -880,6 +928,12 @@ "state" : "translated", "value" : "Nihai" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "最终" + } } } }, @@ -1131,6 +1185,12 @@ "state" : "translated", "value" : "Döngü Durumu Widget'ı" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop 状态小组件" + } } } }, @@ -1432,6 +1492,12 @@ "state" : "translated", "value" : "Suprascriere presetată" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预设覆盖" + } } } }, @@ -1662,6 +1728,12 @@ "state" : "translated", "value" : "Start" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "开始" + } } } }, @@ -1781,6 +1853,12 @@ "state" : "translated", "value" : "U" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "U" + } } } } diff --git a/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme b/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme index e2a1660877..6ab6be0246 100644 --- a/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme +++ b/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme @@ -94,6 +94,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "43A943711B926B7B0051FA24" BuildableName = "WatchApp.app" + BlueprintName = "WatchApp" ReferencedContainer = "container:Loop.xcodeproj"> @@ -111,6 +112,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "43A943711B926B7B0051FA24" BuildableName = "WatchApp.app" + BlueprintName = "WatchApp" ReferencedContainer = "container:Loop.xcodeproj"> diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 0fe4d44399..8f46faeb80 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -578,6 +578,12 @@ "state" : "translated", "value" : "." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "." + } } } }, @@ -1310,6 +1316,12 @@ "state" : "translated", "value" : "%1$1@%2$2@" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$1@%2$2@" + } } } }, @@ -1910,7 +1922,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ operează cu bucla închisă în poziția OFF. Pompa și senzorul CGM vor continua să funcționeze, dar aplicația nu va ajusta automat dozarea." + "value" : "%1$@ operează cu bucla închisă în poziția OPRIT. Pompa și senzorul de glicemie vor continua să funcționeze, dar aplicația nu va ajusta automat dozarea." } }, "ru" : { @@ -2179,6 +2191,12 @@ "state" : "translated", "value" : "%1$@ Zaman Ayarlarına Dikkat Edilmesi Gerekiyor" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ 时间设置需要注意" + } } } }, @@ -2931,6 +2949,12 @@ "state" : "translated", "value" : "%1$@ %2$@ içinde çalışmayı durduracaktır. Bundan önce yeni bir provizyon profili ile güncelleme yapmanız gerekecektir." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ 在 %2$@中将无法正常工作。您需要在该版本发布之前,使用新的配置文件进行更新。" + } } } }, @@ -3139,6 +3163,12 @@ "state" : "translated", "value" : "%1$@: %2$@ %3$@" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@: %2$@ %3$@" + } } } }, @@ -3197,6 +3227,12 @@ "state" : "translated", "value" : "⚠️" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "⚠️" + } } } }, @@ -4043,6 +4079,12 @@ "state" : "translated", "value" : "%@ 'ın yeni bir sürümü mevcut ve uygulamayı kullanmaya devam etmeniz önerilir." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 已推出新版本,建议您继续使用该应用。" + } } } }, @@ -4380,7 +4422,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "按照推荐的大剂量输注" + "value" : "接受推荐的大剂量输注" } } } @@ -5298,7 +5340,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Adăugați CGM" + "value" : "Adăugați senzor de glicemie" } }, "ru" : { @@ -5360,17 +5402,35 @@ "state" : "translated", "value" : "Adăugați elementul pe rândul de jos" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "将项目添加到底行" + } } } }, "Add item to Lock Screen / CarPlay display" : { "comment" : "Title for Add item", "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter un élément à l'écran de verrouillage / CarPlay" + } + }, "ro" : { "stringUnit" : { "state" : "translated", "value" : "Adăugați element la ecranul de blocare / afișajul CarPlay" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "向锁屏 / CarPlay显示 添加项" + } } } }, @@ -5520,6 +5580,12 @@ "state" : "translated", "value" : "Adăugați linie predictivă" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加预测线" + } } } }, @@ -5822,6 +5888,12 @@ "state" : "translated", "value" : "için düzeltilmiş" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已调整" + } } } }, @@ -6397,6 +6469,12 @@ "state" : "translated", "value" : "Toate alertele sunt dezactivate până la" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "所有警报均已静音,直至……" + } } } }, @@ -6692,7 +6770,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Un modificator activ a schimbat raportul dintre carbohidrați și insulină cât și sensibilitatea la insulină. Dacă nu doriți ca acest lucru să vă afecteze calculul bolusului și gllicemia prognozată, luați în considerare dezactivarea modificatorului." + "value" : "O suprascriere activă a schimbat raportul dintre carbohidrați și insulină cât și sensibilitatea la insulină. Dacă nu doriți ca acest lucru să vă afecteze calculul bolusului și gllicemia prognozată, luați în considerare dezactivarea suprascrierii." } }, "ru" : { @@ -6884,6 +6962,12 @@ "state" : "translated", "value" : "Manuel KŞ girişinizi kaydetmeye çalışırken bir hata oluştu." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在尝试保存您手动输入的血糖值时发生错误。" + } } } }, @@ -6955,6 +7039,12 @@ "state" : "translated", "value" : "Beklenmeyen bir ekleme hatası durumu oluştu." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "初始化向导发生意外错误状态" + } } } }, @@ -7959,7 +8049,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Sunteți sigur că vreți să ștergeți acest CGM?" + "value" : "Sunteți sigur că vreți să ștergeți acest senzor de glicemie?" } }, "ru" : { @@ -8155,7 +8245,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Are you sure you want to delete this service?" + "value" : "您确定要删除此服务吗?" } } } @@ -9121,6 +9211,12 @@ "state" : "translated", "value" : "Bluetooth Kapalı Uyarısı" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "蓝牙关闭提醒" + } } } }, @@ -9210,6 +9306,12 @@ "state" : "translated", "value" : "Bluetooth Kullanılamıyor Uyarısı" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "蓝牙不可用提示" + } } } }, @@ -10213,7 +10315,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Întrerupere Bolus" + "value" : "Întrerupere bolus" } }, "ru" : { @@ -10445,7 +10547,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Intrare Carbohidrați" + "value" : "Intrare carbohidrați" } }, "ru" : { @@ -10589,6 +10691,12 @@ "state" : "translated", "value" : "Karb Oranı Programı" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "碳水系数配置表" + } } } }, @@ -10930,6 +11038,12 @@ "state" : "translated", "value" : "Karbonhidrat Girişi Çok Büyük" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "碳水化合物输入量过大" + } } } }, @@ -11238,6 +11352,12 @@ "state" : "translated", "value" : "Atenție" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "注意" + } } } }, @@ -11841,7 +11961,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Verificați sursa de date CGM" + "value" : "Verificați sursa de date a senzorului de glicemie" } }, "ru" : { @@ -11944,6 +12064,12 @@ "state" : "translated", "value" : "Aygıtınızın saatini kontrol edin ve/veya tüm geçersiz verileri Apple Sağlık'tan kaldırın." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请检查设备时间,并/或从“苹果健康”App中删除任何无效数据。" + } } } }, @@ -12102,7 +12228,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Alegeți Preferatul:" + "value" : "Alegeți preferatul:" } }, "zh-Hans" : { @@ -12179,7 +12305,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Închide" + "value" : "Închideți" } }, "ru" : { @@ -12199,6 +12325,12 @@ "state" : "translated", "value" : "Kapat" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关闭" + } } } }, @@ -12470,7 +12602,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Bucla închisă necesită o sesiune activă de senzor CGM" + "value" : "Bucla închisă necesită o sesiune activă a unui senzor de glicemie" } }, "ru" : { @@ -12807,6 +12939,12 @@ "state" : "translated", "value" : "từ lúc %1$@" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自从 %1$@" + } } } }, @@ -13016,6 +13154,12 @@ "state" : "translated", "value" : "Kurulumu Tamamla" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "完成设置" + } } } }, @@ -13272,6 +13416,12 @@ "Configure Display" : { "comment" : "Title for the view to configure the lock screen display", "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configurer l'affichage" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -13296,10 +13446,22 @@ "comment" : "A link that takes the user to a view where they can configure the display of the live activity screen on their lock screen and in CarPlay.", "isCommentAutoGenerated" : true, "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configurer l'écran de verrouillage / CarPlay" + } + }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Configurați ecranul de blocare / rândul Carplay" + "value" : "Configurați ecranul de blocare / rândul CarPlay" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "配置锁屏 / CarPlay 显示行" } } } @@ -13639,7 +13801,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Interval țintă pentru corecție" + "value" : "Interval de corecție" } }, "ru" : { @@ -13716,7 +13878,13 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Eșec la reporni %1$@" + "value" : "Nu s-a putut reporni %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法重启 %1$@" } } } @@ -13890,6 +14058,12 @@ "state" : "translated", "value" : "Kritik Olay Günlüğü Hazır" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关键事件日志已就绪" + } } } }, @@ -13973,6 +14147,12 @@ "state" : "translated", "value" : "Kritik Olay Günlükleri" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关键事件日志" + } } } }, @@ -14056,6 +14236,12 @@ "state" : "translated", "value" : "Kritik Olay Günlükleri dışa aktarılamadı." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法导出关键事件日志。" + } } } }, @@ -14232,7 +14418,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Glicemia actuală de %1$@ se situează sub intervalul țintă de corecție." + "value" : "Glicemia actuală de %1$@ se situează sub intervalul de corecție." } }, "ru" : { @@ -14364,7 +14550,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Modificare personalizată" + "value" : "Suprascriere personalizată" } }, "ru" : { @@ -14488,7 +14674,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Presetare particularizată" + "value" : "Presetare personalizată" } }, "ru" : { @@ -14758,6 +14944,12 @@ "state" : "translated", "value" : "dB" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "dB" + } } } }, @@ -14926,7 +15118,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Ștergeți „%@”?" + "value" : "Ștergeți \"%@\"?" } }, "zh-Hans" : { @@ -15260,7 +15452,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Ștergeți CGM" + "value" : "Ștergeți senzor de glicemie" } }, "ru" : { @@ -15296,7 +15488,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "删除CGM数据源" + "value" : "删除CGM" } } } @@ -15344,6 +15536,12 @@ "state" : "translated", "value" : "Ștergeți alimentele" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除食物" + } } } }, @@ -15538,7 +15736,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Ștergeți date CGM de testare" + "value" : "Ștergeți datele senzorului de test" } }, "ru" : { @@ -15726,7 +15924,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Ștergeți date pompă de testare" + "value" : "Ștergeți datele pompei de test" } }, "ru" : { @@ -15815,7 +16013,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Livrează" + "value" : "Administrați" } }, "ru" : { @@ -16068,6 +16266,12 @@ "state" : "translated", "value" : "Diyabet Tedavisi" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "糖尿病治疗" + } } } }, @@ -16430,6 +16634,12 @@ "state" : "translated", "value" : "Afișați opțiunile de control" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示控制选项" + } } } }, @@ -16588,6 +16798,12 @@ "state" : "translated", "value" : "Tamamlandı" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "完成" + } } } }, @@ -16837,6 +17053,12 @@ "state" : "translated", "value" : "Süre aşıldı: %1$.1f saat" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "持续时间超过: %1$.1f 小时" + } } } }, @@ -16906,7 +17128,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Activați Bluetooth" + "value" : "Activați\nBluetooth" } }, "ru" : { @@ -16979,6 +17201,12 @@ "state" : "translated", "value" : "Activați aplicarea parțială pe bază de glicemie" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用基于血糖的部分输注" + } } } }, @@ -17026,6 +17254,12 @@ "state" : "translated", "value" : "Activați corecția retrospectivă integrală" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用整体回溯校正(IRC)" + } } } }, @@ -17882,6 +18116,12 @@ "state" : "translated", "value" : "Günlükleri Dışa Aktarırken Hata" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出日志发生错误" + } } } }, @@ -18394,6 +18634,12 @@ "state" : "translated", "value" : "İzin verilen maksimum karbonhidrat miktarını aşıyor" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "超过最大允许碳水化合物摄入量" + } } } }, @@ -18685,6 +18931,12 @@ "state" : "translated", "value" : "Dışa Aktar- %1$@" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出-%1$@" + } } } }, @@ -19101,6 +19353,12 @@ "state" : "translated", "value" : "Parmak Ucu KŞ" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "指尖血糖" + } } } }, @@ -19184,6 +19442,12 @@ "state" : "translated", "value" : "Bildirimleri, Kritik Uyarıları ve Zamana Duyarlı Bildirimleri AÇIK konuma getirerek şimdi düzeltin." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请立即开启“通知”、“重要警报”和“时效性通知”以解决问题。" + } } } }, @@ -19475,6 +19739,12 @@ "state" : "translated", "value" : "В целях безопасности вам следует разрешить критические оповещения, срочные оповещения и уведомления (некритические оповещения) на вашем устройстве, чтобы продолжить использовать %1$@ и не отключать отдельные тревоги." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "出于安全考虑,您应允许设备上的“关键警报”、“时效性警报”和“通知权限”(非关键警报),以便继续使用 %1$@ ,且无法关闭单个警报。" + } } } }, @@ -19634,6 +19904,12 @@ "state" : "translated", "value" : "Tahmini KŞ" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预测血糖值" + } } } }, @@ -19687,6 +19963,12 @@ "state" : "translated", "value" : "Часто задаваемые вопросы об оповещениях" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关于警报的常见问题" + } } } }, @@ -20067,6 +20349,12 @@ "state" : "translated", "value" : "Aplicare parțială pe bază de glicemie" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "基于血糖的部分输注" + } } } }, @@ -20388,6 +20676,12 @@ "state" : "translated", "value" : "KŞ Verileri Artık Kullanılabilir" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "现已获取血糖数据" + } } } }, @@ -20720,6 +21014,12 @@ "state" : "translated", "value" : "KŞ Hedef Aralığı Programı" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "血糖目标范围表" + } } } }, @@ -20772,6 +21072,12 @@ "state" : "translated", "value" : "АППАРАТНЫЕ ЗВУКИ" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "泵体提示音" + } } } }, @@ -20824,6 +21130,12 @@ "state" : "translated", "value" : "Высокий уровень глюкозы" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "高血糖" + } } } }, @@ -20876,6 +21188,12 @@ "state" : "translated", "value" : "Как отключить некритичные оповещения?" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "如何关闭非关键警报?" + } } } }, @@ -20928,6 +21246,12 @@ "state" : "translated", "value" : "Как отключить только срочные и некритические предупреждения?" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "如何仅屏蔽“时效性”和“非关键”警报?" + } } } }, @@ -20981,6 +21305,12 @@ "state" : "translated", "value" : "Как временно отключить все звуки приложения %1$@?" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "如何暂时关闭 %1$@ 应用中的所有声音?" + } } } }, @@ -21064,6 +21394,12 @@ "state" : "translated", "value" : "Nasıl güncellenir (LoopDocs)" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "如何更新(LoopDocs)" + } } } }, @@ -21105,6 +21441,12 @@ "state" : "translated", "value" : "Sunt un tester" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "我是一名测试员" + } } } }, @@ -21158,6 +21500,12 @@ "state" : "translated", "value" : "Если режим фокусировки iOS включен, а функция отключения оповещений выключена, критические оповещения будут по-прежнему доставляться, а некритические будут глушиться до тех пор, пока %1$@ не будет добавлен в каждый режим фокусировки в качестве разрешенного приложения." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "如果 iOS 专注模式处于开启状态,且“静音提醒”处于关闭状态,则关键提醒仍会正常推送,而非关键提醒将被静音,直到将 %1$@ 添加到每个专注模式的“允许的应用”列表中为止。" + } } } }, @@ -21280,7 +21628,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "În versiunile viitoare ale Loop, aceste experimente se pot schimba, pot deveni părți standard ale algoritmului Loop sau pot fi eliminate complet din Loop. Vă rugăm să urmăriți chat-ul Loop Zulip pentru a fi la curent cu posibilele modificări ale acestor funcții." + "value" : "În versiunile viitoare ale Loop, aceste experimente se pot schimba, pot deveni părți standard ale algoritmului Loop sau pot fi eliminate complet din Loop. Vă rugăm să urmăriți mesageria Loop Zulip pentru a fi la curent cu posibilele modificări ale acestor funcții." } }, "zh-Hans" : { @@ -21757,6 +22105,12 @@ "state" : "translated", "value" : "İnsülin ayarlamaları devre dışı bırakıldı!" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "胰岛素剂量调整功能已被禁用!" + } } } }, @@ -21880,7 +22234,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "已输注胰岛素" + "value" : "胰岛素输注" } } } @@ -22425,6 +22779,12 @@ "state" : "translated", "value" : "İnsülin Duyarlılık Programı" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "胰岛素敏感系数(ISF)方案" + } } } }, @@ -22508,6 +22868,12 @@ "state" : "translated", "value" : "İnsülin Askıya Alındı" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "胰岛素已暂停输注" + } } } }, @@ -22609,6 +22975,12 @@ "state" : "translated", "value" : "İnsülin Tipi" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "胰岛素类型" + } } } }, @@ -22656,6 +23028,12 @@ "state" : "translated", "value" : "Corecție retrospectivă integrală" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "整体回顾性校正 (IRC)" + } } } }, @@ -22703,6 +23081,12 @@ "state" : "translated", "value" : "Corecția Retrospectivă Integrală (CRI) este o extensie a componentei algoritmului standard de Corecție Retrospectivă (CR) din Loop, care ajustează prognoza pe baza istoricului discrepanțelor dintre nivelurile de glicemie prezise și cele reale. \n \nSpre deosebire de CR, care analizează discrepanțele din ultimele 30 de minute, cu CRI, istoricul discrepanțelor se adună în timp. Așadar, discrepanțele pozitive continue în timp vor duce la creșterea dozării. Dacă discrepanțele sunt negative în timp, Loop va reduce și mai mult dozarea." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "整体回顾性校正(IRC)是Loop中标准回顾性校正(RC)算法组件的扩展,它根据预测血糖值与实际血糖值之间的历史偏差来调整预测值。\n\n与仅关注过去30分钟偏差情况的RC不同,IRC会随时间推移累积偏差历史数据。因此,若偏差值长期呈正值,剂量将相应增加;若偏差值长期呈负值,Loop则会进一步减少剂量。" + } } } }, @@ -22786,6 +23170,12 @@ "state" : "translated", "value" : "%1$@ kesintiye uğradı: %3$@ %4$@'nın %2$@ 'si" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "中断的 %1$@:%3$@ %4$@ 的 %2$@" + } } } }, @@ -23196,6 +23586,12 @@ "state" : "translated", "value" : "Geçersiz Gelecek Glikoz" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无效的未来血糖值" + } } } }, @@ -23267,6 +23663,12 @@ "state" : "translated", "value" : "Gelecekte %1$@ olan bir zaman damgasıyla geçersiz KŞ okuması" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "血糖读数无效,其时间戳为 %1$@ ,该时间点在未来" + } } } }, @@ -23319,6 +23721,12 @@ "state" : "translated", "value" : "Критические оповещения iOS и срочные оповещения — это типы уведомлений Apple. Они используются для проведения высокоприоритетных мероприятий. Вот некоторые примеры:" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "iOS 严重告警与时效性告警是苹果系统的两类通知,用于高优先级事件。部分示例包括:" + } } } }, @@ -23371,6 +23779,12 @@ "state" : "translated", "value" : "РЕЖИМЫ ФОКУСИРОВКИ IOS" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "iOS专注模式" + } } } }, @@ -23561,6 +23975,12 @@ "state" : "translated", "value" : "Görünüşe göre yediğiniz bir yemeği kaydetmemiş olabilirsiniz. Şimdi kaydetmek için dokunun." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "检测到你可能进食了但没有记录餐食。点击此处立即录入碳水。" + } } } }, @@ -23738,7 +24158,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Lansează aplicația CGM" + "value" : "Lansează aplicația senzorului de glicemie" } }, "ru" : { @@ -23859,6 +24279,12 @@ "state" : "translated", "value" : "Daha fazla bilgi edin" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "了解更多" + } } } }, @@ -23948,6 +24374,12 @@ "state" : "translated", "value" : "Bir dakikadan az kaldı" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "剩余时间不足 1 分钟" + } } } }, @@ -24118,7 +24550,7 @@ "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Écran verrouillé / Île dynamique / CarPlay" + "value" : "Écran verrouillé / Dynamic Island / CarPlay" } }, "ro" : { @@ -24126,6 +24558,12 @@ "state" : "translated", "value" : "Ecran de blocare / Insulă dinamică / CarPlay" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "锁屏 / 灵动岛 / CarPlay 车载" + } } } }, @@ -24189,7 +24627,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Înregistrați Doză" + "value" : "Înregistrați doza" } }, "ru" : { @@ -24298,6 +24736,12 @@ "state" : "translated", "value" : "Kayıtlı İnsülin Dozu" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "记录的胰岛素剂量" + } } } }, @@ -24594,7 +25038,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Eșec Buclă" + "value" : "Eșec buclă" } }, "ru" : { @@ -24689,7 +25133,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Loop a detectat o problemă cu setările Bluetooth și nu va funcționa cu succes până când Bluetooth nu este activat. Nu veţi primi valori ale glicemiei sau nu veţi putea face bolus." + "value" : "Loop a detectat o problemă cu setările Bluetooth și nu va funcționa cu succes până când Bluetooth nu este activat. Nu veți primi valori ale glicemiei sau nu veți putea face bolus." } }, "ru" : { @@ -24938,7 +25382,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "În mod normal, Loop furnizează 40% din necesarul de insulină estimat pentru fiecare ciclu de dozare. \n \nCând experimentul Aplicare parțială bazată pe glicemie este activat, Loop o să varieze procentul de bolus recomandat administrat pentru fiecare ciclu în funcție de nivelul glicemiei.\n \nÎn apropierea intervalului de corecție, va utiliza 20% (similar cu o bazală temporar) și va crește treptat până la maximum 80% la glicemie ridicată (200 mg/dl, 11,1 mmol/l). \n \nVă rugăm să rețineți că în timpul creșterii rapide a glicemiei, cum ar fi după o masă neanunțată, această funcție, combinată cu efectele vitezei și ale corecției retrospective, poate duce la o doză mai mare decât ar necesita ISF-ul dumneavoastră." + "value" : "În mod normal, Loop furnizează 40% din necesarul de insulină estimat pentru fiecare ciclu de dozare. \n \nCând algoritmul experimental Aplicare parțială bazată pe glicemie este activat, Loop o să varieze procentul de bolus recomandat administrat pentru fiecare ciclu în funcție de nivelul glicemiei.\n \nÎn apropierea intervalului de corecție, va utiliza 20% (similar cu o bazală temporar) și va crește treptat până la maximum 80% la glicemie ridicată (200 mg/dl, 11,1 mmol/l). \n \nVă rugăm să rețineți că în timpul creșterii rapide a glicemiei, cum ar fi după o masă neanunțată, această funcție, combinată cu efectele vitezei și ale corecției retrospective, poate duce la o doză mai mare decât ar necesita sensibilitatea dumneavoastră la insulină." } }, "zh-Hans" : { @@ -25112,6 +25556,12 @@ "state" : "translated", "value" : "Bluetooth etkinleştirilene kadar döngü başarılı bir şekilde çalışmayacaktır. KŞ ölçümleri alamayacak veya bolus yapamayacaksınız." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在启用蓝牙之前,Loop 无法正常工作。您将无法获取血糖读数,也不能执行大剂量输注。" + } } } }, @@ -25426,6 +25876,12 @@ "state" : "translated", "value" : "Управление оповещениями" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理警报" + } } } }, @@ -26065,7 +26521,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Notificări Mese Neanunțate" + "value" : "Notificări mese neanunțate" } }, "ru" : { @@ -26281,6 +26737,12 @@ "state" : "translated", "value" : "Ayarlarda izin verilen maksimum bolus eksik" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "设置中缺少最大剂量限制" + } } } }, @@ -27119,6 +27581,12 @@ "state" : "translated", "value" : "Negatif süreye izin verilmez" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不允许出现负时长" + } } } }, @@ -27286,6 +27754,12 @@ "state" : "translated", "value" : "Nightscout" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nightscout" + } } } }, @@ -27333,6 +27807,12 @@ "state" : "translated", "value" : "Nicio alertă nu va suna când este dezactivat sunetul. Odată ce această perioadă se încheie, alertele și alarmele dumneavoastră vor relua normal." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "静音期间所有提醒与告警都不会发出声音。请选择你希望保持静音的时长。" + } } } }, @@ -28396,6 +28876,12 @@ "state" : "translated", "value" : "Bildirim gönderimi, telefonunuzun ayarlarında Zamanlanmış Özet olarak ayarlanmıştır.\n\n%1$@ adresinden bildirim almakta gecikmeyi önlemek için bildirim gönderimini Anında Gönderim olarak ayarlanmasını öneririz." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "你的手机设置中,该 App 通知投递方式为定时推送摘要。\n\n为避免接收 %1$@ App 通知出现延迟,建议将通知投递方式改为立即推送。" + } } } }, @@ -28542,7 +29028,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Notificări Întârziate" + "value" : "Notificări întârziate" } }, "ru" : { @@ -28556,6 +29042,12 @@ "state" : "translated", "value" : "Bildirimler Gecikti" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "通知延迟" + } } } }, @@ -28823,6 +29315,12 @@ "state" : "translated", "value" : "Kapalı" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关闭" + } } } }, @@ -28900,6 +29398,12 @@ "state" : "translated", "value" : "Oh hayır! Dozlama sırasında Loop çöktü ve bu iletişim kutusu kapatılana kadar insülin ayarlamaları duraklatıldı. Dozlama geçmişi doğru olmayabilir. Lütfen İnsülin İletim tablolarını gözden geçirin ve kan şekerinizi dikkatle izleyin." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "糟糕!Loop 在给药过程中发生故障,胰岛素调整功能已暂停,直至关闭此对话框。给药记录可能不准确。请查看胰岛素输注图表,并密切监测您的血糖水平。" + } } } }, @@ -30097,6 +30601,12 @@ "state" : "translated", "value" : "описание прогнозирования с помощью интегральной ретроспективной коррекции" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "积分效应: %1$@\n总葡萄糖效应: %2$@" + } } } }, @@ -30312,6 +30822,12 @@ "state" : "translated", "value" : "Kritik Olay Günlüklerinin Hazırlanması" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在准备关键事件日志" + } } } }, @@ -30484,6 +31000,12 @@ "state" : "translated", "value" : "Profilin süresi doluyor" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "配置文件到期 " + } } } }, @@ -30561,6 +31083,12 @@ "state" : "translated", "value" : "Profil Yakında Sona Eriyor" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "配置文件即将到期" + } } } }, @@ -31039,6 +31567,12 @@ "state" : "translated", "value" : "Pump Event" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "胰岛素泵事件" + } } } }, @@ -31097,6 +31631,12 @@ "state" : "translated", "value" : "Срок работы помпы закончился" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "泵已过期" + } } } }, @@ -31293,6 +31833,12 @@ "state" : "translated", "value" : "Pompa Yöneticisi Hatası: %1$@" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "泵管理器错误:%1$@" + } } } }, @@ -31760,6 +32306,12 @@ "state" : "translated", "value" : "Pompa Askıya Alındı. Otomatik dozlama devre dışı bırakıldı." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "泵已暂停。自动给药功能已关闭。" + } } } }, @@ -32853,6 +33405,12 @@ "state" : "translated", "value" : "Uzak Karbonhidrat Girişi: %d gram" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "远程录入碳水:%d 克" + } } } }, @@ -33030,6 +33588,12 @@ "state" : "translated", "value" : "Резервуар пуст" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "储药器已空" + } } } }, @@ -33332,6 +33896,12 @@ "state" : "translated", "value" : "Salvați" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存" + } } } }, @@ -33604,6 +34174,12 @@ "state" : "translated", "value" : "Planlanan" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预设" + } } } }, @@ -33628,6 +34204,12 @@ "state" : "translated", "value" : "Selectați opțiunile de afișare ale ecranului de blocare" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择锁屏显示选项" + } } } }, @@ -33738,6 +34320,12 @@ "state" : "translated", "value" : "Ошибка сенсора" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "传感器故障" + } } } }, @@ -34059,6 +34647,12 @@ "state" : "translated", "value" : "Kurulum Tamamlanmadı" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "设置未完成" + } } } }, @@ -34267,6 +34861,12 @@ "state" : "translated", "value" : "Basit Bolus Hesaplayıcı" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "简易大剂量计算器" + } } } }, @@ -34356,6 +34956,12 @@ "state" : "translated", "value" : "Basit Yemek Hesaplayıcı" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "简易进餐计算器" + } } } }, @@ -34575,7 +35181,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "URL site" + "value" : "Adresă site" } }, "ru" : { @@ -34607,6 +35213,12 @@ "state" : "translated", "value" : "Site URL" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "网址" + } } } }, @@ -34684,6 +35296,12 @@ "state" : "translated", "value" : "Yazılım güncellemesi" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "软件更新" + } } } }, @@ -35088,6 +35706,12 @@ "state" : "translated", "value" : "Destek" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "支持" + } } } }, @@ -35296,6 +35920,12 @@ "state" : "translated", "value" : "Treceți la principală" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换到 main (主) 分支" + } } } }, @@ -35365,7 +35995,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Atingeți aici pentru a configura un CGM" + "value" : "Atingeți aici pentru a configura un senzor de glicemie" } }, "ru" : { @@ -35990,6 +36620,12 @@ "state" : "translated", "value" : "Atingeți Activare sunet pentru a relua sunetul pentru alerte și alarme." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点击“取消静音”即可恢复提醒和警报的声音。" + } } } }, @@ -36049,6 +36685,12 @@ "state" : "translated", "value" : "TestFlight" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "TestFlight" + } } } }, @@ -36108,6 +36750,12 @@ "state" : "translated", "value" : "Срок действия TestFlight" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "TestFlight 到期 " + } } } }, @@ -36398,6 +37046,12 @@ "state" : "translated", "value" : "Bolus dozlama algoritması, bazal hızınızı ayarlamak için kullanılandan daha ihtiyatlı bir kan şekeri tahmini kullanır. \n\n Sonuç olarak, bir bolustan sonra tahmin edilen kan şekeriniz, hedef aralığınızdan daha yüksek olabilir." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "大剂量给药算法对预测血糖的估算,会比调整基础率时更加保守。\n\n因此,注射大剂量之后的预测血糖,仍有可能高于你的目标区间。" + } } } }, @@ -37170,7 +37824,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Setări Terapie" + "value" : "Setări terapie" } }, "ru" : { @@ -37225,6 +37879,12 @@ "state" : "translated", "value" : "Aceasta este versiunea de dezvoltare a Loop, construită din ramura dev. Orice actualizări ale acestei ramuri pot conține funcții noi, netestate și pot fi nesigure. Dacă nu sunteți tester, vă rugăm să nu utilizați această ramură și să treceți la ramura principală." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "这是 Loop 的开发版本,由 dev(开发)分支编译构建。该分支上的更新可能包含尚未经过测试的新功能,并且可能存在安全风险。如果你不是测试人员,请不要使用此分支,请切换至 main(主)分支。" + } } } }, @@ -37270,7 +37930,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Această opțiune se aplică numai atunci când Strategia de dozare a Loop este setată pe Bolus automat." + "value" : "Această opțiune se aplică numai atunci când strategia de dozare a Loop este setată pe bolus automat." } }, "ru" : { @@ -37328,7 +37988,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Alerte Urgente" + "value" : "Alerte urgente" } }, "ru" : { @@ -37336,6 +37996,12 @@ "state" : "translated", "value" : "Срочные оповещения" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "时效性警报" + } } } }, @@ -37399,7 +38065,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Notificări Urgente" + "value" : "Notificări urgente" } }, "ru" : { @@ -37471,6 +38137,12 @@ "state" : "translated", "value" : "Батарейка трансмиттера садится" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "发射器电量低" + } } } }, @@ -37619,6 +38291,12 @@ "state" : "translated", "value" : "Выключите громкость на устройстве iOS или добавьте %1$@ в качестве разрешенного приложения для каждого режима фокусировки. Срочные и критические оповещения по-прежнему будут звучать, но некритические оповещения будут отключены." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请关闭 iOS 设备的音量,或将“ %1$@ ”添加到每个“专注模式”的允许应用列表中。时间敏感型和关键警报仍会发出声音,但非关键警报将被静音。" + } } } }, @@ -37708,6 +38386,12 @@ "state" : "translated", "value" : "Uyarıları, alarmları veya sensör KŞ okumalarını almak için Bluetooth'u açın." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开蓝牙以接收提醒、警报或传感器血糖读数。" + } } } }, @@ -37910,6 +38594,12 @@ "state" : "translated", "value" : "Uyarı Silinemiyor" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法清除警报" + } } } }, @@ -37999,6 +38689,12 @@ "state" : "translated", "value" : "Pompaya Ulaşılamıyor" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法连接到泵" + } } } }, @@ -38183,6 +38879,12 @@ "state" : "translated", "value" : "Manuel KŞ Girişi Kaydedilemiyor" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法保存手动输入的血糖值" + } } } }, @@ -38272,6 +38974,12 @@ "state" : "translated", "value" : "Devam etmekte olan bolus durdurulamıyor. iPhone'unuzu pompaya yaklaştırın ve tekrar deneyin. Ayrıntılar için insülin uygulama geçmişinizi kontrol edin ve kan şekerinizi yakından takip edin." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法终止正在进行的大剂量输注。请将 iPhone 靠近胰岛素泵后重试。查看胰岛素输注历史获取详情,并密切监测你的血糖。" + } } } }, @@ -38563,6 +39271,12 @@ "state" : "translated", "value" : "Bilinmeyen ön ayar: %1$@" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未知预设:%1$@" + } } } }, @@ -39249,6 +39963,12 @@ "state" : "translated", "value" : "Очень низкий" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "紧急低" + } } } }, @@ -39427,6 +40147,12 @@ "state" : "translated", "value" : "Используйте функцию отключения оповещений. Она позволяет временно отключить все ваши оповещения и тревоги с помощью кнопки %1$@ , включая критические оповещения и срочные оповещения." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用“静音提醒”功能。它允许您暂时关闭所有%1$@ 应用程序提醒和警报。 包括关键警报和时间敏感警报。" + } } } }, @@ -39642,6 +40368,12 @@ "state" : "translated", "value" : "Egzersiz Ön Ayarını Kullan" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用运动预设" + } } } }, @@ -39780,6 +40512,12 @@ "value" : "Warnung" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avertissement" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -39791,6 +40529,12 @@ "state" : "translated", "value" : "Avertizare" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "警告" + } } } }, @@ -39868,6 +40612,12 @@ "state" : "translated", "value" : "Uyarı! Güvenlik bildirimleri KAPALI" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "警告!安全提醒已关闭" + } } } }, @@ -39920,6 +40670,12 @@ "state" : "translated", "value" : "Каковы примеры критических и срочных оповещений?" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "紧急警告与时间敏感通知分别有哪些示例?" + } } } }, @@ -40306,6 +41062,12 @@ "state" : "translated", "value" : "Uygulama, Kapalı Döngü modundan çıktığında, tipik bir pompa gibi basitleştirilmiş bir bolus hesaplayıcı kullanır." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "退出闭环模式时,该应用会使用一套简化的大剂量计算器,和普通胰岛素泵的逻辑一致。" + } } } }, @@ -40359,6 +41121,12 @@ "state" : "translated", "value" : "Пока отключение звука оповещений включено, все оповещения от вашего %1$@ приложения, включая критические и срочные оповещения, будут временно отображаться без звуков и будут только вибрировать." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "开启静音提醒后,所有来自您的%1$@App 提醒都将无法接收。 包含紧急和限时警报的应用程序将暂时不发出声音,仅振动显示。" + } } } }, @@ -40403,7 +41171,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "În timp ce alertele silențioase sunt activate, este posibil ca pompa de insulină și hardware-ul CGM să sune în continuare." + "value" : "În timp ce alertele silențioase sunt activate, este posibil ca pompa de insulină și aparatura senzorului de glicemie să sune în continuare." } }, "ru" : { @@ -40470,6 +41238,12 @@ "state" : "translated", "value" : "При попытке перезапустить %1$@ произошла ошибка. \n\n %2$@" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "尝试重启时 %1$@ 发生错误。\n\n%2$@" + } } } }, @@ -40678,6 +41452,12 @@ "state" : "translated", "value" : "Geçici Egzersiz Ayarı 24 saatten uzun süredir açık. Hala etkin olmasını istediğinizden emin olun veya uygulamadan kapatın." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "运动临时调整模式已开启超过 24 小时。请确认你确实仍需要启用该模式,或在 App 内将其关闭。" + } } } }, @@ -40767,6 +41547,12 @@ "state" : "translated", "value" : "Geçici Egzersiz Ayarı Hala Açık" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "运动临时调整模式仍处于开启状态" + } } } }, @@ -40919,7 +41705,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Este posibil să nu primiți alerte sonore, vizuale sau cu vibrații referitoare la informații critice de siguranță. \n\nPentru a remedia problema, atingeți „Setări” și asigurați-vă că notificările, alertele critice și notificările urgente sunt activate." + "value" : "Este posibil să nu primiți alerte sonore, vizuale sau cu vibrații referitoare la informații critice de siguranță. \n\nPentru a remedia problema, atingeți \"Setări\" și asigurați-vă că notificările, alertele critice și notificările urgente sunt activate." } }, "ru" : { @@ -41002,7 +41788,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Ora %1$@ dumneavoastră a fost schimbată. %2$@ are nevoie de înregistrări precise ale timpului pentru a face predicții despre glicemia și pentru a ajusta insulina în consecință. \n\nVerificați setările %1$@ (General / Data și Ora) și verificați dacă „Setare automată” este activată. Nerezolvarea poate duce la o administrare insuficientă sau excesivă de insulină." + "value" : "Ora %1$@ dumneavoastră a fost schimbată. %2$@ are nevoie de înregistrări precise ale timpului pentru a face predicții despre glicemia și pentru a ajusta insulina în consecință. \n\nVerificați setările %1$@ (General / Data și Ora) și verificați dacă \"Setare automată\" este activată. Nerezolvarea poate duce la o administrare insuficientă sau excesivă de insulină." } }, "ru" : { @@ -41016,6 +41802,12 @@ "state" : "translated", "value" : "%1$@ 'inizin saati değiştirildi. %2$@ KŞ ile ilgili tahminlerde bulunmak ve insülininizi buna göre ayarlamak için doğru zaman kayıtlarına ihtiyaç duyar. \n\n %1$@ Ayarlarınızı (Genel / Tarih ve Saat) kontrol edin ve 'Otomatik Olarak Ayarla' seçeneğinin AÇIK olduğunu doğrulayın. Çözülmemesi, insülinin ciddi şekilde yetersiz veya fazla verilmesine yol açabilir." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "你的%1$@设备时间已被修改。%2$@ 需要准确的时间记录,以此预测血糖变化并相应调整胰岛素输注。\n\n请前往%1$@的设置(通用‑日期与时间),确认「自动设置」已开启。若该问题得不到解决,可能造成胰岛素严重输注不足或过量输注。" + } } } }, diff --git a/Loop/mul.lproj/Main.xcstrings b/Loop/mul.lproj/Main.xcstrings index 4316a6468f..6ac8e058a7 100644 --- a/Loop/mul.lproj/Main.xcstrings +++ b/Loop/mul.lproj/Main.xcstrings @@ -398,7 +398,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "预计" + "value" : "预测" } } } @@ -530,7 +530,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "详细" + "value" : "详情" } } } @@ -2707,7 +2707,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "g CLB" + "value" : "g COB" } }, "ru" : { diff --git a/LoopCore/Localizable.xcstrings b/LoopCore/Localizable.xcstrings index a3ccfb14d6..9777757d5f 100644 --- a/LoopCore/Localizable.xcstrings +++ b/LoopCore/Localizable.xcstrings @@ -172,6 +172,12 @@ "value" : "Aktive Kohlenhydrate (COB)" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucides actifs (COB)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -263,6 +269,12 @@ "Active Insulin (IOB)" : { "comment" : "Description for the Insulin On Board selection for the Live Activity configuration", "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insuline active (IOB)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -291,6 +303,12 @@ "state" : "translated", "value" : "la" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 %1$@" + } } } }, @@ -421,11 +439,23 @@ "COB" : { "comment" : "Label used for the Carbohydrates On Board value in the Live Activity view", "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucides" + } + }, "ro" : { "stringUnit" : { "state" : "translated", "value" : "COB" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "活性碳水化合物 (COB)" + } } } }, @@ -504,6 +534,12 @@ "Current Glucose (Value and Arrow)" : { "comment" : "Description for the Current Glucose selection for the Live Activity configuration", "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glycémie actuelle (valeur et flèche)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -538,17 +574,35 @@ "state" : "translated", "value" : "Delta" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta (变化值)" + } } } }, "Delta Glucose (Delta)" : { "comment" : "Description for the Delta Glucose selection for the Live Activity configuration", "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta Glucose (Delta)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", "value" : "Delta glicemie (Delta)" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "血糖变化量(Delta)" + } } } }, @@ -633,11 +687,23 @@ "value" : "Prognostizierter Glukosewert (Endgültiger Blutzuckerwert)" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glycémie prévue (glycémie finale)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", "value" : "Glicemie prognozată (glicemie eventuală)" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预测血糖(最终稳态血糖)" + } } } }, @@ -686,6 +752,12 @@ "value" : "IOB" } }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insuline" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -697,6 +769,12 @@ "state" : "translated", "value" : "IOB" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "活性胰岛素 (IOB)" + } } } }, @@ -780,6 +858,12 @@ "state" : "translated", "value" : "Rată bazală relativă (bazală)" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "相对基础率(基础率)" + } } } }, diff --git a/LoopUI/InfoPlist.xcstrings b/LoopUI/InfoPlist.xcstrings index 796050aaed..a6ceca4250 100644 --- a/LoopUI/InfoPlist.xcstrings +++ b/LoopUI/InfoPlist.xcstrings @@ -100,6 +100,12 @@ "state" : "translated", "value" : "LoopUI" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoopUI" + } } } }, diff --git a/LoopUI/Localizable.xcstrings b/LoopUI/Localizable.xcstrings index a21b1ce012..e6a1a5bc0c 100644 --- a/LoopUI/Localizable.xcstrings +++ b/LoopUI/Localizable.xcstrings @@ -132,7 +132,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "\n%1$@\n\nAtingeți pictogramele de stare ale CGM și ale pompei de insulină pentru mai multe informații. %2$@ va continua să încerce să finalizeze o buclă, dar verificați eventualele probleme de comunicare cu pompa și CGM-ul dumneavoastră." + "value" : "\n%1$@\n\nAtingeți pictogramele de stare ale senzorului de glicemie și ale pompei de insulină pentru mai multe informații. %2$@ va continua să încerce să finalizeze o buclă, dar verificați eventualele probleme de comunicare cu pompa și senzorul dumneavoastră de glicemie." } }, "ru" : { @@ -209,7 +209,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "\n%1$@\n\n Atingeți pictogramele de stare pentru CGM și pompa de insulină pentru mai multe informații. %2$@ va continua să încerce să finalizeze o buclă, dar urmăriți eventualele probleme de comunicare cu pompa și CGM." + "value" : "\n%1$@\n\n Atingeți pictogramele de stare ale senzorului de glicemie și ale pompei de insulină pentru mai multe informații. %2$@ va continua să încerce să finalizeze o buclă, dar urmăriți eventualele probleme de comunicare cu pompa și cu senzorul de glicemie." } }, "ru" : { @@ -286,7 +286,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "\n%1$@ operează cu bucla închisă în poziția OFF. Pompa și CGM vor continua să funcționeze, dar aplicația nu va ajusta automat dozarea. \n\n %2$@" + "value" : "\n%1$@ operează cu bucla închisă în poziția OFF. Pompa și senzorul de glicemie vor continua să funcționeze, dar aplicația nu va ajusta automat dozarea. \n\n %2$@" } }, "ru" : { @@ -685,7 +685,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "%@ 单位" + "value" : "%@ U" } } } @@ -1823,6 +1823,12 @@ "state" : "translated", "value" : "dB" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "dB" + } } } }, @@ -2453,6 +2459,12 @@ "state" : "translated", "value" : "Döngü Uyarısı" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop 警告" + } } } }, @@ -3382,6 +3394,12 @@ "state" : "translated", "value" : "U" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "U" + } } } }, diff --git a/WatchApp Extension/Localizable.xcstrings b/WatchApp Extension/Localizable.xcstrings index 2ed6d420bf..d82c452516 100644 --- a/WatchApp Extension/Localizable.xcstrings +++ b/WatchApp Extension/Localizable.xcstrings @@ -913,6 +913,12 @@ "state" : "translated", "value" : "Bolus" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "大剂量" + } } } }, @@ -1371,6 +1377,12 @@ "state" : "translated", "value" : "dB" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "dB" + } } } }, @@ -1817,6 +1829,12 @@ "state" : "translated", "value" : "iPhone'unuzun yakında olduğundan emin olun ve tekrar deneyin." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请确保您的 iPhone 在附近,然后重试。" + } } } }, @@ -2287,6 +2305,12 @@ "state" : "translated", "value" : "Kapalı" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关闭" + } } } }, @@ -2965,6 +2989,12 @@ "state" : "translated", "value" : "ön ayar" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预设" + } } } }, @@ -3300,6 +3330,12 @@ "state" : "translated", "value" : "Önerilen: %@ Ü" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "推荐: %@ 单位" + } } } }, @@ -3383,6 +3419,12 @@ "state" : "translated", "value" : "Önerilen: Hesaplanıyor..." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "REC:正在计算……" + } } } }, @@ -4043,6 +4085,12 @@ "state" : "translated", "value" : "U" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "U" + } } } }, @@ -4245,6 +4293,12 @@ "state" : "translated", "value" : "iPhone'a Ulaşılamıyor" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法连接 iPhone" + } } } }, diff --git a/WatchApp Extension/ckcomplication.xcstrings b/WatchApp Extension/ckcomplication.xcstrings index bbde2258f7..91ee7a8fd1 100644 --- a/WatchApp Extension/ckcomplication.xcstrings +++ b/WatchApp Extension/ckcomplication.xcstrings @@ -82,6 +82,12 @@ "state" : "translated", "value" : "3DK" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "3分钟" + } } } }, @@ -166,6 +172,12 @@ "state" : "translated", "value" : "120" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "120" + } } } }, @@ -250,6 +262,12 @@ "state" : "translated", "value" : "120↘︎" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "120↘︎" + } } } }, @@ -333,6 +351,12 @@ "state" : "translated", "value" : "%@%@ %@" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@%@ %@" + } } } } diff --git a/WatchApp/mul.lproj/Interface.xcstrings b/WatchApp/mul.lproj/Interface.xcstrings index 75cecdb708..f8cbb9f6f7 100644 --- a/WatchApp/mul.lproj/Interface.xcstrings +++ b/WatchApp/mul.lproj/Interface.xcstrings @@ -1396,7 +1396,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Override" + "value" : "覆盖" } } } From c1c170a6d9fa663bcb742a21b321168bc3186013 Mon Sep 17 00:00:00 2001 From: Eric Jensen Date: Wed, 9 Sep 2026 17:43:50 -0400 Subject: [PATCH 10/26] Increase spacing and size of main screen Liquid Glass toolbar icons (#2504) * Even out and space the status screen toolbar icons On iOS 26 the five toolbar items share one Liquid Glass background with no spacing between them. Any space item inserted between them splits that shared background into separate pills, so the gaps here are transparent padding baked into each rendered icon instead, which widens the spacing while leaving the single pill intact. Sizing normalizes on each icon's visible artwork rather than its canvas. The stock assets pad themselves by very different amounts -- bolus has essentially none, carbs has ~3.7pt top and bottom, and settings is a 25x25 gear centered on a 25x40 canvas -- so matching canvas sizes leaves the glyphs visibly uneven and gives settings a noticeably taller pill. The ink bounds were measured by rasterizing the assets at 600dpi and taking the alpha bounding box; artwork is centered in every one to within 0.6pt, so scaling alone aligns them. Also records the pre-meal item's index at setup instead of hardcoding it per OS version, since the two layouts place it differently. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019hVEziK3QFghqmf2Hq4YAw * Measure icon ink at runtime and clear the 44pt tap target Two follow-ups to the icon sizing in this branch. Replace the ToolbarLayout.Ink table with an alpha-channel scan of each image, so toolbarIcon() no longer takes an ink: argument and the five measured constants go away. Same result, but it cannot go stale if an asset is redrawn. Measuring also yields the ink's origin, so the icon is centered on its artwork rather than on its canvas -- these assets are centered to within 0.6pt, but the code no longer depends on that. Raise iconVerticalMargin from 4 to 7 so each rendered canvas is 44pt tall. The horizontal padding already put every item past 44pt wide, but height was inkHeight + 8 = 38pt, under the HIG minimum, and a small tap target is part of what people are reporting. Checked in portrait and landscape on an iPhone 16 Pro. * Normalize the toolbar icon assets and drop the sizing code Put all seven toolbar icon assets on a common 40x40pt canvas with the artwork 30pt tall, so nothing in code needs to know how each asset is padded. The PDFs stay vector: each page is re-placed with a scale and translate through CoreGraphics, not rasterized. The settings gear is the one PNG and is upscaled 1.2x from its @3x source; its compact-height variant is dropped so landscape loads the same asset as portrait. The two selected states are inverted badges, a plate filling the canvas with the glyph knocked out, not line art. Their alpha bounds are the plate, so any scheme that sizes on ink gets them wrong: sizing on the partner's ink (the constants table) grew the pre-meal plate to 40pt on a 38pt item and clipped its corners; sizing on their own ink (the runtime scan) shrank the glyph 20-27% when toggled on. Each badge now uses its unselected partner's transform, so toggling swaps in place with the plate at its natural size. With the assets normalized, toolbarIcon() only centers the image on the item canvas: 44pt for the minimum tap target, plus horizontal room between items since on iOS 26 a space item would split the shared Liquid Glass background. ToolbarLayout.Ink, inkBounds(), inkHeight and the padding constants are gone. --------- Co-authored-by: Claude Opus 5 Co-authored-by: Pete Schwamb --- .../Pre-Meal Selected.pdf | Bin 4645 -> 4648 bytes .../Pre-Meal.imageset/Pre-Meal.pdf | Bin 4506 -> 4483 bytes .../bolus.imageset/bolus.pdf | Bin 3946 -> 3804 bytes .../carbs.imageset/Meal.pdf | Bin 4523 -> 4554 bytes .../settings.imageset/Contents.json | 43 +++++---------- .../settings.imageset/settings.png | Bin 820 -> 1074 bytes .../settings.imageset/settings@2x.png | Bin 1898 -> 2696 bytes .../settings.imageset/settings@3x.png | Bin 2921 -> 5461 bytes .../settings.imageset/settings_compact@2x.png | Bin 1424 -> 0 bytes .../settings.imageset/settings_compact@3x.png | Bin 2223 -> 0 bytes .../workout-selected.pdf | Bin 4299 -> 4184 bytes .../workout.imageset/workout.pdf | Bin 4984 -> 4865 bytes .../StatusTableViewController.swift | 50 ++++++++++++++---- 13 files changed, 52 insertions(+), 41 deletions(-) delete mode 100644 Loop/DerivedAssetsBase.xcassets/settings.imageset/settings_compact@2x.png delete mode 100644 Loop/DerivedAssetsBase.xcassets/settings.imageset/settings_compact@3x.png diff --git a/Loop/DerivedAssetsBase.xcassets/Pre-Meal Selected.imageset/Pre-Meal Selected.pdf b/Loop/DerivedAssetsBase.xcassets/Pre-Meal Selected.imageset/Pre-Meal Selected.pdf index 8c285da0df0a223013d124f21467d9c3aefef557..a55a507ca28647c75489b92e86f414417e004186 100644 GIT binary patch delta 1762 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 diff --git a/Loop/DerivedAssetsBase.xcassets/Pre-Meal.imageset/Pre-Meal.pdf b/Loop/DerivedAssetsBase.xcassets/Pre-Meal.imageset/Pre-Meal.pdf index 0992a4a2860b615d49418e1a454975c881f1b844..76620abf024cd7119bf834053055ae96f289e75d 100644 GIT binary patch delta 1617 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 diff --git a/Loop/DerivedAssetsBase.xcassets/bolus.imageset/bolus.pdf b/Loop/DerivedAssetsBase.xcassets/bolus.imageset/bolus.pdf index c9e69b5cfc093e82080b2390889ae93b949a7148..2b83db1d1e6207ae2ab0255b0386b47245e73eb4 100644 GIT binary patch delta 918 zcmaixT}TvB6vvyivPn0>kI>K#*w%{R+`Y52Gm2R5uBN8Be&mC@KFsc1ZBut_W;QLz zMax16N(ND|dhpGj%pQ^~Oe2KA-iio`pcknJqlbbL?C$!}he4OiB z`&I_a0|GU0jx8=W*O-hbS53lg##yjS5|os%x}g!dlkH&!yfc=0l)UsKUCfnlck|!& zd9U~Ga0M!c*o4`echV+|3H;{1J~HF$dfs1gpl>{7a_IXJt7(3D6r4}B9QI#(IEVT_ zW$r0GU$*>y>23Aym-$mOi<5UdlAiPprlDs8uhU15Bt%!diOnN8Q1xzXbtb>PE~%R= zG=6?+n>sU10_pc|Oo&ah@(Ua)x*E)_8vVL5+)*^Ra3yIqnwb5;wzDGf)pAxS_^R&?x5BzU3LX#*1A9IIr9mKShm%4Zt*C4_rOb)rb}(Iylw^ zw;DERUWXX8`WT;$f(~<#L3@P_70g~HI1IQ?RLpJQNS6m)M92aImJ3~HAD%?F0}`Ih$bAd-2$9jjnH20z?}*y zI{0W5YpUh9wnTF!8Ft0pm~+g*sGAPc2&rLDtRb7H2qWx7g-EhXJQjpzgpSdjM`>uP z5(yFwY@KqjwgH-mrqgGDodrSx2EWY}33Vudg>uQ^&x5*%tQnWU>nV=|>M`V1Yt!+gttoq31EQ_ eA{^jx3H$#m>od{~N-gf3cZi6!TFYw78TKbM=M+Bx delta 1022 zcmah{T}TvB6lU|q#_rOaxhN2#hYFRzupWwvH0`OPL`otHgun;E=pnjy*Y$@Ux(v*{-~GXBqj@_r9`m)c>iXpvl^K`seiPUsd{9uz9t3LFzeccY)%&2?SD$JE-m7dbrn)cxvrYB`MgnTBC3W31egkFeSuw)s03kH!>A}R zd-#G#21CTf97<#63`hxa8dJHYN#bC|q%Sjfh`LyVTQQ1VOuDTY67B1WGqDsq{^3!I zn8{mXDV0)~Wpkxe#KfrGu{Vh+8$eBCj`G=JqBKOp8nZXO_c4Rr3L;b$oi15+U`fTq p;Y6VhhfF%8a7e_W=n?);fp$J1F`fCG5|>KEC6tyP$qLj9e*q}!CF1}9 diff --git a/Loop/DerivedAssetsBase.xcassets/carbs.imageset/Meal.pdf b/Loop/DerivedAssetsBase.xcassets/carbs.imageset/Meal.pdf index 5e1c948daddf721ee3dd86e653fbe87d78ad98be..40d01e73b29b8efe497b153f5637bc5515165f20 100644 GIT binary patch delta 1555 zcmah}X;4#F6b6C_VILM{zoH0KATKXFCa4KZ8I+wMTWLsMLJ$(kCZL$2P#FX?eW1vU zvRJW-C{wgF+FGS(fFdF;R4SkrL8^dQMpP7d5fKSw>3|ATK|lajDw80bENqVc;kXNuWQy6uE4%L)HjeFkc};ES zLW%;u)P$B)3V3$&`EElOR*4_lD$Y6zbJy?E>(PH1Tr*x)C@l<&yf)dFcs%}_>aagW zPBESKeTPd%8tw=&jK-E$*&kIoBI{>rXJO}+>Rb7PN~S2*?CNCY>I05F@s^o2N9?0r zXK$2|Ul?c564l*L`J@>yu=L3>-WE~7!{^=i?%r(s7Ky(Tj#Skv z209uzoO;^%@yi3_ChE}XMDm2G{)8$na?<*5^_JqjYA@dio6uzgo;jJ&XKT3_7EAML@b=<$MZP)1C^R?$Z6{(v`d{&gyv9$Z&}5urY5!ok0lVNe>FvBtLp+;=iS<=uW;yZNtK` z3L$qRr3>Rp=+}8iU+K}YSaL;7ScfX8(xTI7qfdyy;hyGx-57hCm7!kRvZ|XPT+*XF z*=^f%#-fyHbQSL-nyO9TF{D}|AlMjI*`ap61yOJC)Tw>N0;ReR-GKdh2bq zd@IK*kM3-xH+sB~=IGX5T$GX)7n_(poW+UHEodrH5!z?=_a~T} z)p$5xcd;}|OYL}UDt_#6_c?RN&|udK!|&zABlV|ER`d`~u1!#|8ySqWGwW;l^zwag zHm`T6E}g5oe!+NU6#Gg;ORUu+W{)IzVC*wPYRS&s(i5E0e!=Wu>0j$=L*?hMSKu%+ zyHd)-X({5DWg!s5^>PjIT0*_hwwWGOmc7O$=IwC(ynd=n(_1v#!Ula|;0nW9xHf6SR_M4GAYpUFAhH)|>>wMUN0Dwke>`@KI z9j0h4i|hrsaBVJbL#7Ooh&VC?-~thakc&izFu?~_!(=?H@^f!lXBdceBE3&d0=Q5X zi^-89JfJHV4N}kv16N22aGMaGki!(`fFwxkAnGIq;8kB2Z~)kd;R|9)kYEN74@B<9Iwo zqkdw;6Sc$rIF3N0d{U1H5lE%IMFi_({{ah@So{D0 delta 1515 zcmX@5yjppJn1qRffkJ*#7MG2Ug1%2`UV2G}f+js&2iTGc)N{R+FvNQ|+@h5?kj@&HdJ1`zxhB zGT(Mh^7f4n|G!;b+qB``VrvG3I@{`RF#J0{MwjdeV*s6lq2PNA*} zXPd#M7XmREQ*E^$`8YJrE5H4;#3-;i*HZdukEQT}s0Kmbe&e&Jw)0f{ux5SsU&N-q zj!pf3{`U}rXDbA4-K94xs;+Z8>GEfuqR)jAwr0Vu{l0T^ zoww=c_vO8L&*IJBlISS-4o+xw&McdLaOnoJw{F3Zh$ z{hQ}b`CnVP!)d`^%zuCQyovvV-Z^i-2=P6kQ6K-#RoEgU9C1^)rEg-g?B;Z5_+{LgQKCnv3r z5xBqS!=_C-F(KE5j9xEIcsb>b;ug~d7B;)KwaNJl`LQR@k~3UjS5W?_&GLNAT*DoP zM=sYDls-6k_2AnB@^2XgZTg7p%zxGhKf{4v7|C;m3^Z&6je7+LmHTfT(6tkI$ z(PV9Yxyc)tIc<$WNmM~UD784hv?w{XSiuY;;GCaVlA2ditN=>ZKn1?3DVd2*`4tM$ z20#HLOCXBnn(V+LHu)Q~Dx>*i85U^{3y@MSJG;r1{IZi{SY%8rfdXh*!Vsh#Xr7;f zv4Xy1PDyG}USdhAg1$>?S!Qx-kh>EwyKFwd62K(}RteH=WM*i@m713VVw+DE=bchUNz5Kx|}SVr&^@V4w~}2$c#!h_DGL%1UBl4o$s85$U1*l%oZh@sBd!W8IR1681I zghP!j4KXY+wluSxtS2Z_ZvsrG`T4-O1)2{^L!NnQ`M`*Vuw8&rZDWyakz{OUXl7xO zXknOUk(6j@Vw{?sY-VO@Vs4sbY+}bnScyU`C>0c!0OJ@OuqFl;W?ZVOuKrAJTmUDj B8eRYZ diff --git a/Loop/DerivedAssetsBase.xcassets/settings.imageset/Contents.json b/Loop/DerivedAssetsBase.xcassets/settings.imageset/Contents.json index d5ccdb99c9..6444f236f9 100644 --- a/Loop/DerivedAssetsBase.xcassets/settings.imageset/Contents.json +++ b/Loop/DerivedAssetsBase.xcassets/settings.imageset/Contents.json @@ -1,40 +1,23 @@ { - "images" : [ + "images": [ { - "idiom" : "universal", - "filename" : "settings.png", - "scale" : "1x" + "idiom": "universal", + "filename": "settings.png", + "scale": "1x" }, { - "idiom" : "universal", - "filename" : "settings@2x.png", - "scale" : "2x" + "idiom": "universal", + "filename": "settings@2x.png", + "scale": "2x" }, { - "idiom" : "universal", - "filename" : "settings@3x.png", - "scale" : "3x" - }, - { - "idiom" : "universal", - "scale" : "1x", - "height-class" : "compact" - }, - { - "idiom" : "universal", - "filename" : "settings_compact@2x.png", - "height-class" : "compact", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "settings_compact@3x.png", - "height-class" : "compact", - "scale" : "3x" + "idiom": "universal", + "filename": "settings@3x.png", + "scale": "3x" } ], - "info" : { - "version" : 1, - "author" : "xcode" + "info": { + "version": 1, + "author": "xcode" } } \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/settings.imageset/settings.png b/Loop/DerivedAssetsBase.xcassets/settings.imageset/settings.png index 14fc3623dbe31fa52209545f2dc65e0831f20a16..1f2e2d2dcfacbb991e59665bd6c4a018a88c6ae3 100644 GIT binary patch literal 1074 zcmV-21kL-2P)BPq;0B;|!M)arN`2@!di zguEd0(8@ftg$-ktiP)TT-}mw6d)43Fxp(gSoU?c5S6A2dzpnrFyM9mK-~ao+n3PGG zlzk?BInTxpEJY1HSb!gKe#_jhrKgLzwVY4INvNY2GjS+x!Z4b+0kd#a=G1Wn=0#6! zA|q8rrBeD?A~q-E3u9C>*R!!Jxi2sYwp^cTUIP(u%h!Uq$nuVc^}G%$!iuoFYM z_X-x_-MsT^bnG`#k%Ot2QQTjYlnr-yG;?m?+CtCtINed9x2w+v?!_`ZibrsCo)6~z z1~y>>eu^;HgJ)yfL|TPWU%`(o_cqDK3Qqm)aqy?B2p8If;{8!(6C&!lQs>JaYC2hVDW zdg)|k?A)NZks6*e9)o72K3>Esyjgg^wD7ziTk%m0>TRR1%EYBHa2W64ZajzAQ@33i z_~{6Ue7OKuWz3a%wrqqbjs(c3N6`Bnju{I`?!fPWTF_CQz{FTX6JP-}_S+{{^(@%}A<=}Vfcqsq?uHs9<>4#N^`iL!NVQdo^2 zOFKEU1Kk7BxeZt0$W9_v%I6^jWv71_HzykP76XT|5FcPSKEz+qKU{okI4je+IagB0 z=6tgwHnSyksa|yW=3bm^;^18GPD5_z{BX;-nfl$_ZHo8DY4|ou2lmqIYH2mU;n)sz zKNX!@6Z!tJl2Jtu{Gy0>D@d-(oa@`HDy<;dkog~X$O}f4>39PFq$Sj|_}7Y-QOn}L zBOmQ4p0#T;^P{VdJ)vY>_rTfsWrT)0PrbvkJhpBMYa5EK~-b zH(oZ~8zfh0EP3E&JfG_o9nh_dV_eCyUnuB*?^srnDqU9>iob8EokkGc*M^ZV2+*1> z&Q;g9jZzvN8Mp@D#E_}DJ%;Tm(6}+gbtUc!i~giY=TMA%2vcx*`sUiPGV0dd+>~Ox zxZrf%9F6RqTg%8qZ9l7R=jsB5SKzf`+gVcjde-D6c(x#^JrlLpXr s*>6-!-5mUo=#J~>(WFeuq>Li}0VW_ZsxWb>`v3p{07*qoM6N<$f+DgT&j0`b literal 820 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 diff --git a/Loop/DerivedAssetsBase.xcassets/settings.imageset/settings@3x.png b/Loop/DerivedAssetsBase.xcassets/settings.imageset/settings@3x.png index 6ee011f3c9edd0661bf7a62fcd29632d4b68d909..4cea84a02790bbbf97f7fe47250fc38437981be2 100644 GIT binary patch literal 5461 zcmaKwRag@apvAZ0=+Rw*f|Q^esg!h!N;gWEAl+`DbP6&UNJ}>&qepjl4^Rdo-IKWg z``!2ZaN_a2o$vcay?Ut%A!Q*2000nmHKjNIrQiRGnD9S;W+Ofc0MN6kE6MBmE$)5u zPqEPt?vp#v_r5s&D}rI-`~)O?$_b+iPgI6z0hp7mUKm7i&_-%{NISWcaCUG#-m5aW zZ%icMxp4eCqvE1dMp{-2ubk`yuvw?>Qs8h}gigp9l2^+sz) zte82(0cZ|@1CR{|hX0(2FvCE{2~`mJa52mSD>g|QKc2%iJ6HiTEjKWp$YAFp41R-X zhF73C2cA-n=bSq$g$hpfQDa}2$M6(e&S+?b&TzEJ()kHhD`l_fELvIg2G)tHnO`Ib z0+9#0AW=lPh3_AG?c}!_2X259#0*BEm3QRQjMb0!@FZYma_mR9Osk4RNmUe{S|23* zKBPy8$iV6IFOp|4(~$=d_i}^|f*v!V!nE!iOHc$FI=w?xwho`u(-SjVS(cok(%HVk zJqd+CTMoa2DbQfa5n>?$qYg3xgeuLlDUaI6gn8nBqS~RNRKuY(SaPftRsb7-ZSczW z8#g%0A(ogulh4Jb65jA)SWnv~Vp|!p0Bo5ZxchmW% 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 literal 2921 zcmb7`i9ZvJ1I9OJ?pz~`q*8h%a^>0xb8VI-jBLq${$#xJnlmhoN{6E`$8uzja+F(M zi7<0b&Utfgh8#J_5U@{;-w}Q01y(jHaEQ)31nyE`!XF*b>AO_ z-aLH;nizlH+t2!4?TriPT8zF{q(N%Q7yXC(&yRVheAbtb3%v68;+MsTP?IzeHuUEQ8!-7 z7qa4!HB;X)N=;JooM>)c@yFDGdzhFrQ_6@%-@UR*_3EQiH>oV+1!VWSWPOik+Sz}U zfRyW=>L`=Ix1z-5Dm_!>k*w@bfp)0>K9h`wrh*Y-CZ+a0~Q)kw5; zVy1MyUZRGcL6rl98z<~%d|zbT)L0mDMPkkvp}RcIn71cBbM6qp`f!-?gj1^tB zipnTuo+tSoS;M``S7`+rqTw?a&rn=*%?(d@JEDLu;A4*ZhmwOj=QR8vA!^mht5Yet zv5Vt5&{;NvsgP}RTjq2SB&}qwPI}V9P}Zos2i;P5@MyUb__h6r0VHirfRM%VERIQ| z3%&GV?X1A+Kv{0Bt`}H9YAR~u9I4nZnMr@hn>%_eySW9Gkp*sAE$LH?Gvmpm-tz_Y zw4?atL`LuvY3i)662PBkRojs82fzR132h;dqt>I#o)7@@iASSUAlFO`s7>M{M(RtR zeXPp1U&Z*vSJKGr`nJ1EpDfQr4HGJjhCAMqh@7lGN=Qz?+Im6`FbOK}YS%xwo$bz# z-Z$Ox^1Qv8w$F6D?TR}goOoFc1N&)HJ+orO9SiW~hj;nTWX#>2Njij0h!X-^`Ik|f z2xAVH7AnqHf6usDYZeK6<*N{(#65cxi$nvZ6*>)#)A6EFYoge?Bv5Ri*o~|4sf4ta zd!1W)B|KWMGnrP)0ayQ|Y96u6FXK!ooWjPBB~@)d`V5Et<-yv#?z{cSpLWB$8fGcHj4xf=x0>9%cbDVL z#OOvFmWx>unHA)^Mo^I1q`lJ*1FWv;dN<){1e2^pdoV{30Xw|^T%>f|PC%3`7KnG= z`&r-s>J?b=#`0H}zDCj2VD!GSSDoAll-7MW7}D@MYIt2y5& z2E1vLr&}~lRlEPm1>cC(gKu5QT6l|^s1MK!TnL+>;oel2H2J9xpFS6GT}a&DMh{-2 zVly0rt}A&vv}XGc?YT^XYdgl2HZn^gxS@(uOk+cauWp->M3zn?GjZd&f`^hf0xKq_ zYSXD)s5sp=JBLkNdZWDOso{^MYYC4Yw&2utSU!f)&tT75(n}Qps~qkM%(;7@K`|+~ zC_Z}|fu?A>Le8U#qKei$O!g|7-ZlY5i=p(+G02Sxn`F{ht&_@U>X#=WdC0ku%b&nqNw>!0z#!40kJDe+)rDJ1)? z%T(F3+CZMa)Rv^)j=m(QZ&2^<<03PgtzM;EfXEdR8B5S?>pKn)O{a^~fViwplTd zy9OHl>x`#8Pq>xP78#n~*pT0~ZjOLuLYBU0yr}}Ml$oWVl;Y~A@1K6%T8FKoKG}+T zN#CM|GqkVp&C{?3s)=7a1WCV&>s~m~A!ax%$j`_pA-tV+q2Ml5y%E61?nyHb&hD&7 z*iRfO@|f29?@Khs0xoqNVXrxJ*C&+MCp3$;PGlMFkmt4-p(j15yJnP4n?X0ygL&FV z9N$td(jT<=G!KK|&#C$KIwrLnVdX%$BlBZ9ZX&ce_ae(pnrpr69vYQBq#c-g{*KkI zrahXtBOrs9aIgBKDm<7kQmt@M+HakLlH1d&AT*i8QgBZIK}LBHGtfdT^8C@{S`6B%*DL2>KpZ@b?VRerqZ(mOa;$!5Yv|zHmqd`&J*HkcMnq V<9i|e_wTz5u(m*$SDIlH{tqu6lS2Ri diff --git a/Loop/DerivedAssetsBase.xcassets/settings.imageset/settings_compact@2x.png b/Loop/DerivedAssetsBase.xcassets/settings.imageset/settings_compact@2x.png deleted file mode 100644 index ccac27347e3adb0f03004325d66cbf2e0935d92f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1424 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 WOMqbt4i{q!Qv)tlRabvEMlJvXaN+d; delta 1377 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 B@l5~# delta 2091 zcmaJ?3se(l7H+6HV8k`l1BHk%DvuyaW+pG9F)B%TCc4wk!ZTIY)Ig`xyfB*gd``vr) z>~VYPcErw}0f2y7nn2=m0a_rUibLZ8CI*QN{lvxkoZMDb0o(ek(ci)zf695WW|~TSF7`$U%g;^N3Kg3Qea?sOsH)!- zeYtv|p-JzU_)N84Ws|W&Ap7}jxkI!%EAV1&nTFHWG0+{vqdyrVA2&c13zZQ^BTF)Q z?$_j-Vtx0-^chDZO$YxOMU~AxkxLEQBA&PX2rF-pBTd?0wf;o@h{A8v0>#;$`k$r( zANh`o;m;m6q`hc}(uxAul&+2KZu!2XJwF*pzeM5c2oXCWOPmq3--RIPp|))>Kc!J-vlWMX_JkRr|10AhUr_r zA@+9U;gzwYd8y0X zZu9g_;u|Hglh5bv;%4_*Wx1dQwO!+#6U+!bHH0QDFR<$xoLD|=X+=Hu@NwXmCB~5NUSc1=XaQp1xicxX*>A&{%g;nxm%C*x$+iJ=R|b1!_uT6B<8Qrg zdqBVAaENU{ks|-_lkd1EyW0$=Px!fxTFgs*I?lVP*t{)au zYB*KSyL)|Sg?c=XVIQhV9u!gSbCPTm(razcH+fRr9h0Cfp`-)@U@aB&{_qeOS*<9imlUA(ra$+^t6oz_LT6#b5_dY$l zlF0Rc2!DRfovx?}KGL4O{QIrG4KJ4sb1h5nM~NFPjNNNHjt`($f4S+~&mBwAmSu&X z_Nb802^y{I@Z8t3Zp0>}y8XM3S55F5dK(c&;NFG$^O~rZg?C!B`w&z_N0%lV&sy= zxgw?06~RQz$o<+&{XdEqER#JrmSTO-0$5Reov<=*P3>IQLO56cZ%W}1P_oxXW%^Wb zF6SUiw#(VwGO=rLw&nHJ)>OB+=^w7W(1+^3%GEjMg`b!FYqCz*&6@T3b=QO|V}(n# zZ7H?hJ-XHfEprOG+$wu*U)FZo5q6%ib34=%cZa;!BfQkKs6DZD$4I(&Z06seJQ9cR zdYN13qc@b27>`m|9&!U{PJ%IKnsz?nV%X& zg#vVZN&z$xqE+iOGDHio-U58J3dPp706y6;LJ%TXN_^DYfM^f{=pX=d#3UYXMh4$C zGyg&~YFQY90@0Y1z!wl3RugSq*yb=i9zYXrL;b=~35o#tu=LYHfH%H|1v6BU7aDSi z6vHdB_1t$;6vE|1hI-<{ngODm#4*3)N-zt8XVmfFIEE<(@cBLxEg}avWKkF zc3bxL+yITQ)?v9r0ByZeuEh?Dxr8Mt20kPNL54;$K%g7?$P4ulzK#n}gCsH_I1GpY zAkYhfyjVb}PNG4#16ZPF$F`v!Gl~#yt$CAR1q?zgkORUH6XY<#C=hhV?hnj&MBER| zJh@JWXaKV&W;|bm=q0F9trFmtnSC!(0uX})zP|t#lSyX+3g8{a zV6d@-@C#7QU=R#rPJfIsV7i%S=(Aa*hcWga)*(9lr#K9VgH_|>I1mHEO8iHR^*{Eq zu|E2k$DrdWgP{KRhh@;&e-dFZu;RYojA|sxL_}l$9)~HnW5zMCCReMmqC@6d(*jfq zHP!*<<_-{GJ-`L!FiQ@C-W(~&ma`$L1fok38IvQINm()<1eWqh|DS_X-ZqgI#k#^= Q3I>zGCb_x_gMCT=1-M}$CjbBd diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 37751e093b..a9d99f04f9 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -287,13 +287,30 @@ final class StatusTableViewController: LoopChartsTableViewController { private func makeToolbarButton(imageNamed name: String, tintColor: UIColor, action: Selector) -> UIButton { let button = UIButton(type: .system) - button.setImage(UIImage(named: name), for: .normal) + button.setImage(UIImage(named: name)?.toolbarIcon(), for: .normal) button.tintColor = tintColor button.addTarget(self, action: action, for: .touchUpInside) button.constrainToToolbarIconSize() return button } + /// Layout of the items in the bottom toolbar. + /// + /// The icon assets are all drawn on a 40x40pt canvas with their artwork 30pt tall, so + /// nothing here sizes them. The one thing left to do is pad each item out to the 44pt + /// minimum tap target, with horizontal room between items: on iOS 26 the items share one + /// Liquid Glass background and a space item between them would split it, so the gap is + /// transparent padding on the image itself. + /// + /// `fileprivate` so the UIImage helper at the bottom of this file can read it. + fileprivate enum ToolbarLayout { + static let itemSize = CGSize(width: 60, height: 44) + } + + /// Position of the pre-meal item within `toolbarItems`, recorded at setup + /// because it differs between the iOS 26 and legacy layouts. + private var preMealItemIndex: Int = 1 + private func setupToolbarItems() { let carbs = UIBarButtonItem(customView: carbEntryButton) let bolus = UIBarButtonItem(customView: bolusButton) @@ -308,7 +325,7 @@ final class StatusTableViewController: LoopChartsTableViewController { func flexibleSpace() -> UIBarButtonItem { UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) } - + if #available(iOS 26, *) { toolbarItems = [carbs, preMeal, bolus, workout, settings] } else { @@ -324,6 +341,10 @@ final class StatusTableViewController: LoopChartsTableViewController { settings ] } + + // The two layouts put pre-meal at different indices, so record where it + // actually landed rather than hardcoding it. + preMealItemIndex = toolbarItems?.firstIndex { $0 === preMeal } ?? 1 } private func updateToolbarItems() { @@ -335,13 +356,7 @@ final class StatusTableViewController: LoopChartsTableViewController { bolusButton.isEnabled = isPumpOnboarded settingsButton.accessibilityLabel = NSLocalizedString("Settings", comment: "The label of the settings button") - let preMealIndex: Int - if #available(iOS 26, *) { - preMealIndex = 1 - } else { - preMealIndex = 2 - } - toolbarItems![preMealIndex] = createPreMealButtonItem(selected: preMealMode == true && preMealModeAllowed, isEnabled: preMealModeAllowed) + toolbarItems![preMealItemIndex] = createPreMealButtonItem(selected: preMealMode == true && preMealModeAllowed, isEnabled: preMealModeAllowed) updateWorkoutButton(selected: workoutMode == true && workoutModeAllowed, isEnabled: workoutModeAllowed) } @@ -1478,7 +1493,7 @@ final class StatusTableViewController: LoopChartsTableViewController { } private func createPreMealButtonItem(selected: Bool, isEnabled: Bool) -> UIBarButtonItem { - let item = UIBarButtonItem(image: UIImage.preMealImage(selected: selected), style: .plain, target: self, action: #selector(premealButtonTapped(_:))) + let item = UIBarButtonItem(image: UIImage.preMealImage(selected: selected)?.toolbarIcon(), style: .plain, target: self, action: #selector(premealButtonTapped(_:))) item.accessibilityLabel = NSLocalizedString("Pre-Meal Targets", comment: "The label of the pre-meal mode toggle button") if selected { @@ -1495,7 +1510,7 @@ final class StatusTableViewController: LoopChartsTableViewController { } private func updateWorkoutButton(selected: Bool, isEnabled: Bool) { - workoutButton.setImage(UIImage.workoutImage(selected: selected), for: .normal) + workoutButton.setImage(UIImage.workoutImage(selected: selected)?.toolbarIcon(), for: .normal) workoutButton.accessibilityLabel = NSLocalizedString("Workout Targets", comment: "The label of the workout mode toggle button") if selected { @@ -2093,6 +2108,19 @@ private extension UIButton { } } +private extension UIImage { + /// Centers the icon on a transparent canvas of the toolbar item size. See `ToolbarLayout`. + func toolbarIcon() -> UIImage { + let canvas = StatusTableViewController.ToolbarLayout.itemSize + let origin = CGPoint(x: (canvas.width - size.width) / 2, y: (canvas.height - size.height) / 2) + let format = UIGraphicsImageRendererFormat.preferred() + format.opaque = false + return UIGraphicsImageRenderer(size: canvas, format: format).image { _ in + draw(in: CGRect(origin: origin, size: size)) + }.withRenderingMode(.alwaysTemplate) + } +} + extension UIAlertController { func addActivityIndicator() { let frame = CGRect(x: 0, y: 0, width: 40, height: 40) From 2b4cd9a2feaeccf2c5c527a730d406b4d4ceb0c0 Mon Sep 17 00:00:00 2001 From: Marion Barker <19607791+marionbarker@users.noreply.github.com> Date: Thu, 10 Sep 2026 19:25:13 -0700 Subject: [PATCH 11/26] Updated translations from lokalise on Thu Sep 10 13:42:39 PDT 2026 (#2505) --- .../Bootstrap/Localizable.xcstrings | 60 ++++++ Loop/Localizable.xcstrings | 138 ++++++++++++ LoopCore/Localizable.xcstrings | 200 +++++++++++++++++- 3 files changed, 397 insertions(+), 1 deletion(-) diff --git a/Loop Widget Extension/Bootstrap/Localizable.xcstrings b/Loop Widget Extension/Bootstrap/Localizable.xcstrings index 09e4236b3e..5bcc1a496d 100644 --- a/Loop Widget Extension/Bootstrap/Localizable.xcstrings +++ b/Loop Widget Extension/Bootstrap/Localizable.xcstrings @@ -461,6 +461,12 @@ "value" : "%1$@%2$@" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%2$@" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -489,6 +495,12 @@ "value" : "%@U" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@E" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -642,6 +654,12 @@ "value" : "Couleur" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Farge" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -676,6 +694,12 @@ "value" : "Date" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dato" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -835,6 +859,12 @@ "value" : "Fin" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avslutt" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -1076,6 +1106,12 @@ "value" : "Niveau de glycémie" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukosenivå" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -1104,6 +1140,12 @@ "value" : "Plage de glycémie" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoseområde" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -1459,6 +1501,12 @@ "value" : "Ouvrez l'application pour mettre à jour le widget" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Åpne appen for å oppdatere widgeten" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -1487,6 +1535,12 @@ "value" : "Annulation du préréglage" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forhåndsinnstilt overstyring" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -1717,6 +1771,12 @@ "value" : "Démarrer" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start" + } + }, "nl" : { "stringUnit" : { "state" : "translated", diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 8f46faeb80..9882e740dd 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -5397,6 +5397,12 @@ "value" : "Ajouter un élément à la ligne inférieure" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tilføy element til bunnraden" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -5420,6 +5426,12 @@ "value" : "Ajouter un élément à l'écran de verrouillage / CarPlay" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tilføy en enhet til låseskjermen / CarPlay skjermen" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -5575,6 +5587,12 @@ "value" : "Ajouter une ligne prédictive" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tilføy forutsigende linje" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -10056,6 +10074,12 @@ "value" : "Ligne inférieure" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bunnrad" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -10086,6 +10110,12 @@ "value" : "Configuration de la ligne inférieure" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konfigurasjon av bunnraden" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -13416,12 +13446,24 @@ "Configure Display" : { "comment" : "Title for the view to configure the lock screen display", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anzeige konfigurieren" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Configurer l'affichage" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tilpass skjermen" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -13452,6 +13494,12 @@ "value" : "Configurer l'écran de verrouillage / CarPlay" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tilpass låseskjerm / Carplay rad" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -14600,6 +14648,12 @@ "value" : "Préréglage personnalisé" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Engendefinert forhåndsinnstilling" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -16605,6 +16659,12 @@ "value" : "Afficher les couleurs du glucose" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vis fargene for glukose." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -16629,6 +16689,12 @@ "value" : "Options de contrôle de l'affichage" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vis kontrollopsjonene" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -16658,6 +16724,12 @@ "value" : "Afficher la prédiction dans le graphique" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vis prognosen i et diagram." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -16687,6 +16759,12 @@ "value" : "Affiche jusqu'à 4 éléments. La forme abrégée du libellé figure entre parenthèses." } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vis opp til 4 elementer. Visningsetiketten står i parantes." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -17278,6 +17356,12 @@ "value" : "Activé" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivert" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -21430,6 +21514,12 @@ "value" : "Je suis un testeur" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jeg er en tester" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -24398,6 +24488,12 @@ "value" : "Activité en direct" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Live activity" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -24553,6 +24649,12 @@ "value" : "Écran verrouillé / Dynamic Island / CarPlay" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Låseskjerm / Dynamic Island / CarPlay" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -26886,6 +26988,12 @@ "value" : "Mode" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modus" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -34199,6 +34307,12 @@ "value" : "Options d'affichage de l'écran verrouillé" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Velg ut opsjonene til låseskjermvisning" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -35909,6 +36023,12 @@ "value" : "Basculer sur main" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bytt til main" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -37868,6 +37988,12 @@ "value" : "Il s'agit de la version de développement de Loop, compilée à partir de la branche « dev ». Toute mise à jour de cette branche peut contenir de nouvelles fonctionnalités non testées et pourrait présenter des risques. Si vous n'êtes pas testeur, veuillez ne pas utiliser cette branche et passer plutôt à la branche « main »." } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dette er utviklingsversjonen av Loop, bygget fra dev-grenen. Oppdateringer på denne grenen kan inneholde nye, utestede funksjoner og kan være usikre. Hvis du ikke er tester, bør du ikke bruke denne grenen, men bytte til main." + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -39988,6 +40114,12 @@ "value" : "Utiliser la coloration de glycémie" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bruk BG fargelegging" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -40518,6 +40650,12 @@ "value" : "Avertissement" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advarsel" + } + }, "nl" : { "stringUnit" : { "state" : "translated", diff --git a/LoopCore/Localizable.xcstrings b/LoopCore/Localizable.xcstrings index 9777757d5f..8466837d54 100644 --- a/LoopCore/Localizable.xcstrings +++ b/LoopCore/Localizable.xcstrings @@ -143,6 +143,12 @@ "value" : "Glucides actifs" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktive karbohydrater" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -178,6 +184,12 @@ "value" : "Glucides actifs (COB)" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktive karbohydrater (COB)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -209,6 +221,12 @@ "value" : "Glucides actifs" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktive karboer" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -246,6 +264,12 @@ "value" : "Insuline active" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktiv insulin" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -269,12 +293,24 @@ "Active Insulin (IOB)" : { "comment" : "Description for the Insulin On Board selection for the Live Activity configuration", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktives Insulin (IOB)" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Insuline active (IOB)" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktiv insulin (IOB)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -292,12 +328,24 @@ "at" : { "comment" : "Label used for the Updated time value in the Live Activity view", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "um" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "à" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "kl." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -416,6 +464,12 @@ "value" : "Basal" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -439,12 +493,24 @@ "COB" : { "comment" : "Label used for the Carbohydrates On Board value in the Live Activity view", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Glucides" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -474,6 +540,12 @@ "value" : "Glycémie actuelle" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nåværende BG" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -511,6 +583,12 @@ "value" : "Glycémie actuelle" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nåværende glukoseverdi" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -534,12 +612,24 @@ "Current Glucose (Value and Arrow)" : { "comment" : "Description for the Current Glucose selection for the Live Activity configuration", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "aktueller BZ (Wert und Trend)" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Glycémie actuelle (valeur et flèche)" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nåværende Glukose (Verdi og Pil)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -569,6 +659,12 @@ "value" : "Delta" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -586,12 +682,24 @@ "Delta Glucose (Delta)" : { "comment" : "Description for the Delta Glucose selection for the Live Activity configuration", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta-BZ (Delta)" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Delta Glucose (Delta)" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta Glukose (Delta)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -621,6 +729,12 @@ "value" : "Glycémie éventuelle" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eventuell BG" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -658,6 +772,12 @@ "value" : "Éventuellement" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Omsider" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -693,6 +813,12 @@ "value" : "Glycémie prévue (glycémie finale)" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forutsagt glukoseverdi (eventuell BG)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -723,6 +849,12 @@ "value" : "Dans quel mode voulez-vous afficher les Activités en direct" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "I hvilken modus vil du vise Live activity" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -758,6 +890,12 @@ "value" : "Insuline" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -795,6 +933,12 @@ "value" : "Grand" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stor" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -821,7 +965,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Diagramm und Zeile" + "value" : "Diagramm und Text" } }, "fr" : { @@ -830,6 +974,12 @@ "value" : "Ligne et graphique" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Felt og rad" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -847,12 +997,24 @@ "Relative Basal Rate (Basal)" : { "comment" : "Description for the Basal Rate plot selection for the Live Activity configuration", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Relative Basalrate (Basal)" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Débit basal relatif (Basal)" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Netto basaldose (basal)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -870,12 +1032,24 @@ "Row Only" : { "comment" : "Short name to choose the Lock Screen display without the plot", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "nur Text" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ligne seule" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kun rad" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -907,6 +1081,12 @@ "value" : "Petit" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Liten" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -1033,6 +1213,12 @@ "value" : "Mis à jour" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oppdatert" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -1062,6 +1248,12 @@ "value" : "Mis à jour (à)" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oppdatert (sist)" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -1099,6 +1291,12 @@ "value" : "Mis à jour à" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oppdatert sist" + } + }, "ro" : { "stringUnit" : { "state" : "translated", From dd761e85987775fbf2a55a61b1e8b1d87a12b415 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Tue, 15 Sep 2026 21:57:41 -0500 Subject: [PATCH 12/26] Use real bar button items for the status toolbar (#2515) On iOS 27 the five toolbar items no longer fit narrower phones in portrait and UIKit collapses the tail into an overflow menu. Four of the five were UIBarButtonItem(customView:), which has no title, image or action for the menu to show, so the menu was empty and Settings was unreachable without rotating (#2513). The 60x44pt canvases each icon was padded to (#2504) also made the bar wider than it needed to be. Make all five plain UIBarButtonItems with a title, image and action. UIKit now provides the tap target, spacing and a working overflow entry for each, so the padding helper, the custom UIButtons and the index bookkeeping for swapping the pre-meal item go away; the toggle items are updated in place. --- .../StatusTableViewController.swift | 161 ++++-------------- 1 file changed, 36 insertions(+), 125 deletions(-) diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index a9d99f04f9..dd5123587a 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -273,91 +273,63 @@ final class StatusTableViewController: LoopChartsTableViewController { deviceManager.pumpManagerHUDProvider?.visible = active && onscreen } - private lazy var carbEntryButton = makeToolbarButton(imageNamed: "carbs", tintColor: .carbTintColor, action: #selector(userTappedAddCarbs)) - private lazy var bolusButton = makeToolbarButton(imageNamed: "bolus", tintColor: .insulinTintColor, action: #selector(presentBolusScreen)) - private lazy var settingsButton = makeToolbarButton(imageNamed: "settings", tintColor: .secondaryLabel, action: #selector(onSettingsTapped)) - - private lazy var workoutButton: UIButton = { - let button = UIButton(type: .system) - button.tintColor = .glucoseTintColor - button.addTarget(self, action: #selector(toggleWorkoutMode(_:)), for: .touchUpInside) - button.constrainToToolbarIconSize() - return button - }() - - private func makeToolbarButton(imageNamed name: String, tintColor: UIColor, action: Selector) -> UIButton { - let button = UIButton(type: .system) - button.setImage(UIImage(named: name)?.toolbarIcon(), for: .normal) - button.tintColor = tintColor - button.addTarget(self, action: action, for: .touchUpInside) - button.constrainToToolbarIconSize() - return button - } - - /// Layout of the items in the bottom toolbar. - /// - /// The icon assets are all drawn on a 40x40pt canvas with their artwork 30pt tall, so - /// nothing here sizes them. The one thing left to do is pad each item out to the 44pt - /// minimum tap target, with horizontal room between items: on iOS 26 the items share one - /// Liquid Glass background and a space item between them would split it, so the gap is - /// transparent padding on the image itself. - /// - /// `fileprivate` so the UIImage helper at the bottom of this file can read it. - fileprivate enum ToolbarLayout { - static let itemSize = CGSize(width: 60, height: 44) + private lazy var carbsItem = makeToolbarItem(title: NSLocalizedString("Add Meal", comment: "The label of the carb entry button"), image: UIImage(named: "carbs"), tintColor: .carbTintColor, action: #selector(userTappedAddCarbs)) + private lazy var bolusItem = makeToolbarItem(title: NSLocalizedString("Bolus", comment: "The label of the bolus entry button"), image: UIImage(named: "bolus"), tintColor: .insulinTintColor, action: #selector(presentBolusScreen)) + private lazy var settingsItem = makeToolbarItem(title: NSLocalizedString("Settings", comment: "The label of the settings button"), image: UIImage(named: "settings"), tintColor: .secondaryLabel, action: #selector(onSettingsTapped)) + private lazy var preMealItem = makeToolbarItem(title: NSLocalizedString("Pre-Meal Targets", comment: "The label of the pre-meal mode toggle button"), image: UIImage.preMealImage(selected: false), tintColor: .carbTintColor, action: #selector(premealButtonTapped(_:))) + private lazy var workoutItem = makeToolbarItem(title: NSLocalizedString("Workout Targets", comment: "The label of the workout mode toggle button"), image: UIImage.workoutImage(selected: false), tintColor: .glucoseTintColor, action: #selector(toggleWorkoutMode(_:))) + + private func makeToolbarItem(title: String, image: UIImage?, tintColor: UIColor, action: Selector) -> UIBarButtonItem { + let item = UIBarButtonItem(image: image, style: .plain, target: self, action: action) + item.title = title + item.tintColor = tintColor + return item } - /// Position of the pre-meal item within `toolbarItems`, recorded at setup - /// because it differs between the iOS 26 and legacy layouts. - private var preMealItemIndex: Int = 1 - private func setupToolbarItems() { - let carbs = UIBarButtonItem(customView: carbEntryButton) - let bolus = UIBarButtonItem(customView: bolusButton) - let settings = UIBarButtonItem(customView: settingsButton) - - carbs.title = "Add Meal" - - let preMeal = createPreMealButtonItem(selected: false, isEnabled: true) - updateWorkoutButton(selected: false, isEnabled: true) - let workout = UIBarButtonItem(customView: workoutButton) - func flexibleSpace() -> UIBarButtonItem { UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) } if #available(iOS 26, *) { - toolbarItems = [carbs, preMeal, bolus, workout, settings] + toolbarItems = [carbsItem, preMealItem, bolusItem, workoutItem, settingsItem] } else { toolbarItems = [ - carbs, + carbsItem, flexibleSpace(), - preMeal, + preMealItem, flexibleSpace(), - bolus, + bolusItem, flexibleSpace(), - workout, + workoutItem, flexibleSpace(), - settings + settingsItem ] } - - // The two layouts put pre-meal at different indices, so record where it - // actually landed rather than hardcoding it. - preMealItemIndex = toolbarItems?.firstIndex { $0 === preMeal } ?? 1 } private func updateToolbarItems() { let isPumpOnboarded = onboardingManager.isComplete || deviceManager.pumpManager?.isOnboarded == true - carbEntryButton.accessibilityLabel = NSLocalizedString("Add Meal", comment: "The label of the carb entry button") - carbEntryButton.isEnabled = isPumpOnboarded - bolusButton.accessibilityLabel = NSLocalizedString("Bolus", comment: "The label of the bolus entry button") - bolusButton.isEnabled = isPumpOnboarded - settingsButton.accessibilityLabel = NSLocalizedString("Settings", comment: "The label of the settings button") + carbsItem.isEnabled = isPumpOnboarded + bolusItem.isEnabled = isPumpOnboarded + + let preMealSelected = preMealMode == true && preMealModeAllowed + updateToggleItem(preMealItem, image: UIImage.preMealImage(selected: preMealSelected), selected: preMealSelected, isEnabled: preMealModeAllowed) + let workoutSelected = workoutMode == true && workoutModeAllowed + updateToggleItem(workoutItem, image: UIImage.workoutImage(selected: workoutSelected), selected: workoutSelected, isEnabled: workoutModeAllowed) + } - toolbarItems![preMealItemIndex] = createPreMealButtonItem(selected: preMealMode == true && preMealModeAllowed, isEnabled: preMealModeAllowed) - updateWorkoutButton(selected: workoutMode == true && workoutModeAllowed, isEnabled: workoutModeAllowed) + private func updateToggleItem(_ item: UIBarButtonItem, image: UIImage?, selected: Bool, isEnabled: Bool) { + item.image = image + item.isEnabled = isEnabled + if selected { + item.accessibilityTraits.insert(.selected) + item.accessibilityHint = NSLocalizedString("Disables", comment: "The action hint of the workout mode toggle button when enabled") + } else { + item.accessibilityTraits.remove(.selected) + item.accessibilityHint = NSLocalizedString("Enables", comment: "The action hint of the workout mode toggle button when disabled") + } } public var basalDeliveryState: PumpManagerStatus.BasalDeliveryState? = nil { @@ -1492,39 +1464,6 @@ final class StatusTableViewController: LoopChartsTableViewController { deviceManager.analyticsServicesManager.didDisplayBolusScreen() } - private func createPreMealButtonItem(selected: Bool, isEnabled: Bool) -> UIBarButtonItem { - let item = UIBarButtonItem(image: UIImage.preMealImage(selected: selected)?.toolbarIcon(), style: .plain, target: self, action: #selector(premealButtonTapped(_:))) - item.accessibilityLabel = NSLocalizedString("Pre-Meal Targets", comment: "The label of the pre-meal mode toggle button") - - if selected { - item.accessibilityTraits.insert(.selected) - item.accessibilityHint = NSLocalizedString("Disables", comment: "The action hint of the workout mode toggle button when enabled") - } else { - item.accessibilityHint = NSLocalizedString("Enables", comment: "The action hint of the workout mode toggle button when disabled") - } - - item.tintColor = UIColor.carbTintColor - item.isEnabled = isEnabled - - return item - } - - private func updateWorkoutButton(selected: Bool, isEnabled: Bool) { - workoutButton.setImage(UIImage.workoutImage(selected: selected)?.toolbarIcon(), for: .normal) - workoutButton.accessibilityLabel = NSLocalizedString("Workout Targets", comment: "The label of the workout mode toggle button") - - if selected { - workoutButton.accessibilityTraits.insert(.selected) - workoutButton.accessibilityHint = NSLocalizedString("Disables", comment: "The action hint of the workout mode toggle button when enabled") - } else { - workoutButton.accessibilityTraits.remove(.selected) - workoutButton.accessibilityHint = NSLocalizedString("Enables", comment: "The action hint of the workout mode toggle button when disabled") - } - - workoutButton.isEnabled = isEnabled - workoutButton.sizeToFit() - } - @IBAction func premealButtonTapped(_ sender: UIBarButtonItem) { togglePreMealMode(confirm: false) } @@ -1593,13 +1532,7 @@ final class StatusTableViewController: LoopChartsTableViewController { } } else { if FeatureFlags.sensitivityOverridesEnabled { - let overridesIndex: Int - if #available(iOS 26, *) { - overridesIndex = 3 - } else { - overridesIndex = 6 - } - performSegue(withIdentifier: OverrideSelectionViewController.className, sender: toolbarItems![overridesIndex]) + performSegue(withIdentifier: OverrideSelectionViewController.className, sender: workoutItem) } else { presentWorkoutModeAlertController() } @@ -2099,28 +2032,6 @@ final class StatusTableViewController: LoopChartsTableViewController { } } -private extension UIButton { - func constrainToToolbarIconSize() { - setContentHuggingPriority(.required, for: .horizontal) - setContentHuggingPriority(.required, for: .vertical) - setContentCompressionResistancePriority(.required, for: .horizontal) - setContentCompressionResistancePriority(.required, for: .vertical) - } -} - -private extension UIImage { - /// Centers the icon on a transparent canvas of the toolbar item size. See `ToolbarLayout`. - func toolbarIcon() -> UIImage { - let canvas = StatusTableViewController.ToolbarLayout.itemSize - let origin = CGPoint(x: (canvas.width - size.width) / 2, y: (canvas.height - size.height) / 2) - let format = UIGraphicsImageRendererFormat.preferred() - format.opaque = false - return UIGraphicsImageRenderer(size: canvas, format: format).image { _ in - draw(in: CGRect(origin: origin, size: size)) - }.withRenderingMode(.alwaysTemplate) - } -} - extension UIAlertController { func addActivityIndicator() { let frame = CGRect(x: 0, y: 0, width: 40, height: 40) From e3b32c2a8a7767a260aea21167d025d99d26c56c Mon Sep 17 00:00:00 2001 From: Marion Barker <19607791+marionbarker@users.noreply.github.com> Date: Thu, 17 Sep 2026 19:17:01 -0700 Subject: [PATCH 13/26] Updated translations from lokalise on Thu Sep 17 08:17:21 PDT 2026 (#2523) * Updated translations from lokalise on Thu Sep 17 08:17:21 PDT 2026 * do not change WatchApp.xcscheme, Xcode 27 will just change it back --- Loop/Localizable.xcstrings | 2 +- Loop/mul.lproj/Main.xcstrings | 2 +- WatchApp Extension/Localizable.xcstrings | 2 +- WatchApp/mul.lproj/Interface.xcstrings | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 9882e740dd..1af4612a15 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -10929,7 +10929,7 @@ "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Legg til karbohydrater" + "value" : "Legg inn karbohydrater" } }, "nl" : { diff --git a/Loop/mul.lproj/Main.xcstrings b/Loop/mul.lproj/Main.xcstrings index 6ac8e058a7..b0c0479ee4 100644 --- a/Loop/mul.lproj/Main.xcstrings +++ b/Loop/mul.lproj/Main.xcstrings @@ -3211,7 +3211,7 @@ "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Anbefalt Basal" + "value" : "Anbefalt basal" } }, "nl" : { diff --git a/WatchApp Extension/Localizable.xcstrings b/WatchApp Extension/Localizable.xcstrings index d82c452516..4c26d66fe7 100644 --- a/WatchApp Extension/Localizable.xcstrings +++ b/WatchApp Extension/Localizable.xcstrings @@ -732,7 +732,7 @@ "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Legg til karbohydrater" + "value" : "Legg inn karbohydrater" } }, "nl" : { diff --git a/WatchApp/mul.lproj/Interface.xcstrings b/WatchApp/mul.lproj/Interface.xcstrings index f8cbb9f6f7..2de01fea8b 100644 --- a/WatchApp/mul.lproj/Interface.xcstrings +++ b/WatchApp/mul.lproj/Interface.xcstrings @@ -456,7 +456,7 @@ "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Pre-måltid" + "value" : "Før måltid" } }, "nl" : { @@ -1342,7 +1342,7 @@ "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Overstyr" + "value" : "Overstyring" } }, "nl" : { From acb495e3f100f3764744486a7d07c40ee5a679f4 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 17 Sep 2026 21:47:24 -0500 Subject: [PATCH 14/26] Fit five toolbar items at 375pt when linked against the iOS 27 SDK (#2524) An app linked against the iOS 27 SDK gets wider padding around each bar button item on iOS 27. Five items then overflow on 375pt phones (iPhone SE 3rd gen, iPhone 12 mini), leaving three in the bar and the rest behind the overflow menu. The same build linked against SDK 26.5 fits, which is why browser builds (Xcode 26.5) were unaffected and local Xcode 27 builds were not. Remove the standard padding on the five items, which brings them back within 375pt, and give Settings high visibility priority so it is the last item to overflow rather than the first. Both properties are new in SDK 27, so the block is compiled only with Swift 6.4 (Xcode 27); an SDK 26.5 build does not need it. Verified on iPhone SE (3rd generation) and iPhone 12 mini, iOS 27.0, built with Xcode 27: all five items fit. iOS 26.5 is unchanged. --- Loop/View Controllers/StatusTableViewController.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index dd5123587a..3e124fa7f5 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -306,6 +306,14 @@ final class StatusTableViewController: LoopChartsTableViewController { settingsItem ] } +#if compiler(>=6.4) + if #available(iOS 27, *) { + for item in [carbsItem, preMealItem, bolusItem, workoutItem, settingsItem] { + item.isPaddingRemoved = true + } + settingsItem.visibilityPriority = .high + } +#endif } private func updateToolbarItems() { From c2fddb76a20500bbbd0be5d4133d64382feae689 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 19 Sep 2026 18:31:17 -0500 Subject: [PATCH 15/26] Show the development branch warning at most once a week (#2525) Tapping "I'm a tester" records the date; the warning is skipped for seven days after that. "Switch to main" does not record it, so it shows again on the next launch. --- Loop/Managers/DevelopmentBranchAlerter.swift | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Loop/Managers/DevelopmentBranchAlerter.swift b/Loop/Managers/DevelopmentBranchAlerter.swift index c6795f1d05..db557cea1f 100644 --- a/Loop/Managers/DevelopmentBranchAlerter.swift +++ b/Loop/Managers/DevelopmentBranchAlerter.swift @@ -14,8 +14,11 @@ enum DevelopmentBranchAlerter { private static let switchToMainURL = URL(string: "https://loopkit.github.io/loopdocs/faqs/loop-faqs/#how-do-i-return-to-the-released-version")! - /// Presents a blocking warning when this is a build from the development branch. - /// Shown on every launch; the alert can only be dismissed by an explicit choice. + private static let acknowledgedDateKey = "com.loopkit.Loop.DevelopmentBranchAlerter.acknowledgedDate" + private static let reminderInterval: TimeInterval = 7 * 24 * 60 * 60 + + /// Presents a warning when this is a build from the development branch, at most once a week + /// once acknowledged. static func alertIfNeeded(viewControllerToPresentFrom: UIViewController) { guard FeatureFlags.devBranchWarningEnabled else { return @@ -25,6 +28,11 @@ enum DevelopmentBranchAlerter { return } + if let acknowledged = UserDefaults.standard.object(forKey: acknowledgedDateKey) as? Date, + Date().timeIntervalSince(acknowledged) < reminderInterval { + return + } + let alert = UIAlertController( title: NSLocalizedString("Warning", comment: "Title of the warning shown at launch on development builds"), message: NSLocalizedString("This is the development version of Loop, built from the dev branch. Any updates on this branch may contain new, untested features, and may be unsafe. If you are not a tester, please do not use this branch, and switch to main.", comment: "Body of the warning shown at launch on development builds"), @@ -34,7 +42,9 @@ enum DevelopmentBranchAlerter { alert.addAction(UIAlertAction( title: NSLocalizedString("I'm a tester", comment: "Button that dismisses the development build warning"), style: .default, - handler: nil + handler: { _ in + UserDefaults.standard.set(Date(), forKey: acknowledgedDateKey) + } )) alert.addAction(UIAlertAction( From 3be91feebde9fb3e877dd9e16b5ff6d8f6e25bc7 Mon Sep 17 00:00:00 2001 From: Marion Barker <19607791+marionbarker@users.noreply.github.com> Date: Sat, 26 Sep 2026 08:55:45 -0700 Subject: [PATCH 16/26] Updated translations from lokalise on Fri Sep 25 13:06:28 PDT 2026 (#2537) --- Common/nl.lproj/Intents.strings | 10 +- Loop Status Extension/Localizable.xcstrings | 4 +- .../mul.lproj/MainInterface.xcstrings | 4 +- .../Bootstrap/Localizable.xcstrings | 36 + .../xcshareddata/xcschemes/WatchApp.xcscheme | 2 - Loop/InfoPlist.xcstrings | 6 + Loop/Localizable.xcstrings | 770 ++++++++++++++---- Loop/mul.lproj/Main.xcstrings | 16 +- LoopCore/Localizable.xcstrings | 90 +- LoopUI/Localizable.xcstrings | 14 +- WatchApp Extension/Localizable.xcstrings | 20 +- WatchApp/mul.lproj/Interface.xcstrings | 8 +- 12 files changed, 783 insertions(+), 197 deletions(-) diff --git a/Common/nl.lproj/Intents.strings b/Common/nl.lproj/Intents.strings index e72963d83a..2c435a34f5 100644 --- a/Common/nl.lproj/Intents.strings +++ b/Common/nl.lproj/Intents.strings @@ -2,25 +2,25 @@ "9KhaIS" = "Ik heb het programma ingesteld"; /* (No Comment) */ -"80eo5o" = "Kh. Inv. Toevoegen"; +"80eo5o" = "Koolhydraten invoeren"; /* (No Comment) */ "b085BW" = "Ik kon het programma niet instellen."; /* (No Comment) */ -"I4OZy8" = "Override Programma Inschakelen"; +"I4OZy8" = "Override programma inschakelen"; /* (No Comment) */ -"lYMuWV" = "Override Naam"; +"lYMuWV" = "Override naam"; /* (No Comment) */ "nDKAmn" = "Wat is de naam van de override die je zou willen instellen?"; /* (No Comment) */ -"OcNxIj" = "Kh. Inv. Toevoegen"; +"OcNxIj" = "Koolhydraten invoeren"; /* (No Comment) */ -"oLQSsJ" = "Override '${overrideName}' Inschakelen"; +"oLQSsJ" = "Override '${overrideName}' inschakelen"; /* (No Comment) */ "XNNmtH" = "Programma in Loop inschakelen"; diff --git a/Loop Status Extension/Localizable.xcstrings b/Loop Status Extension/Localizable.xcstrings index e264d1eb98..ec27a597f1 100644 --- a/Loop Status Extension/Localizable.xcstrings +++ b/Loop Status Extension/Localizable.xcstrings @@ -590,7 +590,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Actieve Koolhydraten" + "value" : "Actieve koolhydraten" } }, "pl" : { @@ -715,7 +715,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Actieve Insuline" + "value" : "Actieve insuline" } }, "pl" : { diff --git a/Loop Status Extension/mul.lproj/MainInterface.xcstrings b/Loop Status Extension/mul.lproj/MainInterface.xcstrings index bd96f6297f..4a7d785835 100644 --- a/Loop Status Extension/mul.lproj/MainInterface.xcstrings +++ b/Loop Status Extension/mul.lproj/MainInterface.xcstrings @@ -73,7 +73,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Actieve Koolhydraten" + "value" : "Actieve koolhydraten" } }, "pl" : { @@ -312,7 +312,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Actieve Insuline" + "value" : "Actieve insuline" } }, "pl" : { diff --git a/Loop Widget Extension/Bootstrap/Localizable.xcstrings b/Loop Widget Extension/Bootstrap/Localizable.xcstrings index 5bcc1a496d..7c7c01e686 100644 --- a/Loop Widget Extension/Bootstrap/Localizable.xcstrings +++ b/Loop Widget Extension/Bootstrap/Localizable.xcstrings @@ -467,6 +467,12 @@ "value" : "%1$@%2$@" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%2$@" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -501,6 +507,12 @@ "value" : "%@E" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@E" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -1112,6 +1124,12 @@ "value" : "Glukosenivå" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucosewaarde" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -1146,6 +1164,12 @@ "value" : "Glukoseområde" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucosebereik" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -1507,6 +1531,12 @@ "value" : "Åpne appen for å oppdatere widgeten" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open de app om de widget bij te werken" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -1541,6 +1571,12 @@ "value" : "Forhåndsinnstilt overstyring" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override-programma" + } + }, "ro" : { "stringUnit" : { "state" : "translated", diff --git a/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme b/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme index 6ab6be0246..e2a1660877 100644 --- a/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme +++ b/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme @@ -94,7 +94,6 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "43A943711B926B7B0051FA24" BuildableName = "WatchApp.app" - BlueprintName = "WatchApp" ReferencedContainer = "container:Loop.xcodeproj"> @@ -112,7 +111,6 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "43A943711B926B7B0051FA24" BuildableName = "WatchApp.app" - BlueprintName = "WatchApp" ReferencedContainer = "container:Loop.xcodeproj"> diff --git a/Loop/InfoPlist.xcstrings b/Loop/InfoPlist.xcstrings index 7fd3d72113..a7cef2986e 100644 --- a/Loop/InfoPlist.xcstrings +++ b/Loop/InfoPlist.xcstrings @@ -305,6 +305,12 @@ "value" : "Appen bruker NFC til å koble seg sammen med diabetesenheter." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "De app gebruikt NFC om te koppelen met diabetesapparaten." + } + }, "pl" : { "stringUnit" : { "state" : "translated", diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 1af4612a15..b133b77b79 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -184,7 +184,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : " Pre-Meal Programma" + "value" : " Pre-Meal programma" } }, "pl" : { @@ -1004,7 +1004,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "%@ E Totaal" + "value" : "%@ E totaal" } }, "pl" : { @@ -1237,7 +1237,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ %2$@ kon je huidige tijdelijke basaalsnelheid niet annuleren, die hoger is dan de nieuwe Maximale Basaallimiet die je hebt ingesteld. Dit kan leiden tot een hogere insulinetoediening dan gewenst. \n\nOverweeg om de insulinetoediening handmatig te onderbreken en dan direct het hervatten van de basaaltoediening uit te laten voeren met de nieuwe geldende limiet." + "value" : "%1$@ %2$@ kon je huidige tijdelijke basaalstand niet annuleren, die hoger is dan de nieuwe maximale basaallimiet die je hebt ingesteld. Dit kan leiden tot een hogere insulinetoediening dan gewenst. \n\nOverweeg om de insulinetoediening handmatig te onderbreken en dan direct het hervatten van de basaaltoediening uit te laten voeren met de nieuwe geldende limiet." } }, "pl" : { @@ -1311,6 +1311,12 @@ "value" : "%1$1@%2$2@" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$1@%2$2@" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -1910,7 +1916,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ werkt met Gesloten Loop in de UIT stand. Je pomp en CGM blijven werken, maar de app past de dosering niet automatisch aan." + "value" : "%1$@ werkt met Actieve loop UIT. Je pomp en CGM blijven werken, maar de app past de dosering niet automatisch aan." } }, "pl" : { @@ -2165,7 +2171,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ Tijdinstellingen Hebben Aandacht Nodig" + "value" : "%1$@ Tijdinstellingen hebben aandacht nodig" } }, "pl" : { @@ -2855,6 +2861,12 @@ "value" : "%1$@ vil slutte å fungere i %2$@. Du må gjenoppbygge før det." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ stopt over %2$@ met werken. Bouw de app vóór die tijd opnieuw." + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -4281,6 +4293,12 @@ "value" : "Absorpsjonstid" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opnametijd" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -4493,7 +4511,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Actieve Koolhydraten" + "value" : "Actieve koolhydraten" } }, "pl" : { @@ -4625,7 +4643,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Actieve Koolhydraten: %@" + "value" : "Actieve koolhydraten: %@" } }, "pl" : { @@ -4750,7 +4768,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Actieve Koolhydraten" + "value" : "Actieve koolhydraten" } }, "pl" : { @@ -4875,7 +4893,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Actieve Insuline" + "value" : "Actieve insuline" } }, "pl" : { @@ -5007,7 +5025,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Actieve Insuline: %@" + "value" : "Actieve insuline: %@" } }, "pl" : { @@ -5403,6 +5421,12 @@ "value" : "Tilføy element til bunnraden" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Item toevoegen aan onderste rij" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -5420,6 +5444,12 @@ "Add item to Lock Screen / CarPlay display" : { "comment" : "Title for Add item", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Element zum Sperrbildschirm / CarPlay-Display hinzufügen" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -5432,6 +5462,12 @@ "value" : "Tilføy en enhet til låseskjermen / CarPlay skjermen" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Item toevoegen aan toegangsscherm / CarPlay" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -5512,7 +5548,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Voeg Maaltijd toe" + "value" : "Voeg maaltijd toe" } }, "pl" : { @@ -5593,6 +5629,12 @@ "value" : "Tilføy forutsigende linje" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voorspellingslijn toevoegen" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -5673,7 +5715,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Voeg Pomp toe" + "value" : "Voeg pomp toe" } }, "pl" : { @@ -5786,7 +5828,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Service Toevoegen" + "value" : "Service toevoegen" } }, "pl" : { @@ -6052,7 +6094,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Toestemming Meldingen" + "value" : "Toestemming meldingen" } }, "pl" : { @@ -6132,6 +6174,12 @@ "value" : "Varselstillatelser og demp varsler" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toestemming voor waarschuwingen en dempen" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -6200,7 +6248,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Algoritme Experimenten" + "value" : "Algoritme experimenten" } }, "pl" : { @@ -6259,7 +6307,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Algoritme Experimenten zijn optionele aanpassingen aan het Loop Algoritme. Deze aanpassingen zijn minder grondig getest dan het standaard Loop Algoritme, dus gebruik het voorzichtig." + "value" : "Algoritme-experimenten zijn optionele aanpassingen aan het Loop-algoritme. Deze aanpassingen zijn minder grondig getest dan het standaard Loop-algoritme, dus gebruik ze voorzichtig." } }, "pl" : { @@ -6417,6 +6465,12 @@ "value" : "Alle varsler er dempet" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle waarschuwingen gedempt" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -6476,6 +6530,12 @@ "value" : "Alle varsler er dempet inntil" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle waarschuwingen gedempt tot" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -6529,6 +6589,12 @@ "value" : "Alle favoritter" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle favorieten" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -6588,6 +6654,12 @@ "value" : "Mengde karbohydrater\n(Mengde inntatt)" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gegeten hoeveelheid" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -7228,7 +7300,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "API Key" + "value" : "API sleutel" } }, "pl" : { @@ -7942,7 +8014,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Weet je zeker dat je al je %@ Gegevens wilt verwijderen?\n(Deze actie is niet omkeerbaar)" + "value" : "Weet je zeker dat je al je %@-gegevens wilt verwijderen?\n(Deze actie is niet omkeerbaar)" } }, "pl" : { @@ -8140,6 +8212,12 @@ "value" : "Er du sikker på at du vil slette denne maten?" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weet je zeker dat je dit voedingsmiddel wilt verwijderen?" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -8459,7 +8537,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Authenticeer om %@ E te Bolussen" + "value" : "Authenticeer om %@ E te bolussen" } }, "pl" : { @@ -8566,7 +8644,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Authenticeer om %@ Eenheden te registreren" + "value" : "Authenticeer om %@ eenheden te registreren" } }, "pl" : { @@ -8673,7 +8751,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Basaalsnelheidschema" + "value" : "Basaalstandenschema" } }, "pl" : { @@ -8811,7 +8889,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Basaalsnelheden" + "value" : "Basaalstanden" } }, "pl" : { @@ -9102,7 +9180,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bluetooth\nNiet Beschikbaar" + "value" : "Bluetooth\nniet beschikbaar" } }, "pl" : { @@ -9197,7 +9275,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bluetooth Uit Melding" + "value" : "Bluetooth uit melding" } }, "pl" : { @@ -9292,7 +9370,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bluetooth Niet Beschikbaar Melding" + "value" : "Bluetooth niet beschikbaar melding" } }, "pl" : { @@ -9589,7 +9667,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Aanbevolen Bolus Bijgewerkt" + "value" : "Aanbevolen bolus bijgewerkt" } }, "pl" : { @@ -9773,7 +9851,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bolus Te Klein" + "value" : "Bolus te klein" } }, "pl" : { @@ -10080,6 +10158,12 @@ "value" : "Bunnrad" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Onderste rij" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -10116,6 +10200,12 @@ "value" : "Konfigurasjon av bunnraden" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Onderste rij instellen" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -10327,7 +10417,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bolus Annuleren" + "value" : "Bolus annuleren" } }, "pl" : { @@ -10639,6 +10729,12 @@ "value" : "Antall karbohydrater" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoeveelheid koolhydraten" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -10935,7 +11031,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Kh. Inv. Toevoegen" + "value" : "Koolhydraten toevoegen" } }, "pl" : { @@ -11042,7 +11138,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Koolhydraatinvoer Te Veel" + "value" : "Koolhydraatinvoer te veel" } }, "pl" : { @@ -11268,7 +11364,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Opgenomen Koolhydraten (g) ÷ Koolhydraatratio (g/E) × Insulinegevoeligheid (%1$@/E)" + "value" : "Opgenomen koolhydraten (g) ÷ koolhydraatratio (g/E) × insulinegevoeligheid (%1$@/E)" } }, "pl" : { @@ -12249,6 +12345,12 @@ "value" : "Velg favoritt:" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kies favoriet:" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -12430,7 +12532,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gesloten Loop" + "value" : "Actieve loop" } }, "pl" : { @@ -12531,7 +12633,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gesloten Loop UIT" + "value" : "Actieve loop UIT" } }, "pl" : { @@ -12620,7 +12722,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gesloten Loop vereist een actieve CGM Sensorsessie" + "value" : "Actieve loop vereist een lopende CGM-sensorsessie" } }, "pl" : { @@ -12703,7 +12805,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gesloten Loop vereist dat Installatie is Voltooid" + "value" : "Actieve loop vereist dat de installatie is voltooid" } }, "pl" : { @@ -13057,7 +13159,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Ok" + "value" : "OK" } }, "pl" : { @@ -13158,7 +13260,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Volledige Installatie" + "value" : "Volledige installatie" } }, "pl" : { @@ -13467,7 +13569,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Scherm Configureren" + "value" : "Scherm configureren" } }, "ro" : { @@ -13488,6 +13590,12 @@ "comment" : "A link that takes the user to a view where they can configure the display of the live activity screen on their lock screen and in CarPlay.", "isCommentAutoGenerated" : true, "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sperrbildschirm / CarPlay-Zeile konfigurieren" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -13500,6 +13608,12 @@ "value" : "Tilpass låseskjerm / Carplay rad" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rij voor toegangsscherm / CarPlay instellen" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -13580,7 +13694,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Ga Verder" + "value" : "Ga verder" } }, "pl" : { @@ -13705,7 +13819,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Continue Glucose Monitor" + "value" : "Continue glucose monitor" } }, "pl" : { @@ -13917,6 +14031,12 @@ "value" : "Kunne ikke starte %1$@ på nytt" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kan %1$@ niet herstarten" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -13985,7 +14105,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Kritieke Meldingen" + "value" : "Kritieke meldingen" } }, "pl" : { @@ -14074,7 +14194,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Kritieke Gebeurtenislogboek Klaar" + "value" : "Kritieke gebeurtenislogboek klaar" } }, "pl" : { @@ -14163,7 +14283,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Kritieke Gebeurtenislogboek" + "value" : "Kritieke gebeurtenislogboek" } }, "pl" : { @@ -14252,7 +14372,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Kritieke Gebeurtenislogboek konden niet worden geëxporteerd." + "value" : "Kritieke gebeurtenislogboeken konden niet worden geëxporteerd." } }, "pl" : { @@ -14341,7 +14461,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Huidige Glucose" + "value" : "Huidige glucose" } }, "pl" : { @@ -14654,6 +14774,12 @@ "value" : "Engendefinert forhåndsinnstilling" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aangepast programma" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -14716,7 +14842,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Aangepast Programma" + "value" : "Aangepast programma" } }, "pl" : { @@ -15163,6 +15289,12 @@ "value" : "Slette \"%@\"?" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "„%@” verwijderen?" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -15250,7 +15382,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Verwijder Account" + "value" : "Verwijder account" } }, "pl" : { @@ -15369,7 +15501,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Verwijder Alles" + "value" : "Verwijder alles" } }, "pl" : { @@ -15579,6 +15711,12 @@ "value" : "Slett mat" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voedingsmiddel verwijderen" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -15666,7 +15804,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Service Verwijderen" + "value" : "Service verwijderen" } }, "pl" : { @@ -15872,7 +16010,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Testgegevens Verwijderen" + "value" : "Testgegevens verwijderen" } }, "pl" : { @@ -15966,7 +16104,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Testpompgegevens Verwijderen" + "value" : "Testpompgegevens verwijderen" } }, "pl" : { @@ -16665,6 +16803,12 @@ "value" : "Vis fargene for glukose." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kleuren tonen voor glucose" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -16683,6 +16827,12 @@ "comment" : "A section header in the live activity settings view, describing the control options for displaying glucose levels on the live activity screen.", "isCommentAutoGenerated" : true, "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Optionen zur Anzeigesteuerung" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -16695,6 +16845,12 @@ "value" : "Vis kontrollopsjonene" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weergaveopties" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -16730,6 +16886,12 @@ "value" : "Vis prognosen i et diagram." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voorspelling tonen in grafiek" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -16765,6 +16927,12 @@ "value" : "Vis opp til 4 elementer. Visningsetiketten står i parantes." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toon maximaal 4 items. Het label staat tussen haakjes." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -16838,7 +17006,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gereed" + "value" : "Voltooid" } }, "pl" : { @@ -17268,6 +17436,12 @@ "value" : "Aktiver delvis anvendelse basert på glukose" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucose-afhankelijke gedeeltelijke toediening inschakelen" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17321,6 +17495,12 @@ "value" : "Aktivere integrert retrospektiv korrigering" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Integrale retrospectieve correctie inschakelen" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -17644,7 +17824,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bolus Invoeren" + "value" : "Bolus invoeren" } }, "pl" : { @@ -17739,7 +17919,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Vul Vingerprikglucose In" + "value" : "Vul vingerprikglucose in" } }, "pl" : { @@ -18067,7 +18247,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Fout bij Annuleren Bolus" + "value" : "Fout bij annuleren bolus" } }, "pl" : { @@ -18282,7 +18462,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Fout Bij Hervatten" + "value" : "Fout bij hervatten" } }, "pl" : { @@ -18793,7 +18973,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Overschrijdt Maximale Bolus" + "value" : "Overschrijdt maximale bolus" } }, "pl" : { @@ -18894,7 +19074,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Exporteer Kritieke Gebeurtenislogboek" + "value" : "Exporteer kritieke gebeurtenislogboek" } }, "pl" : { @@ -19072,7 +19252,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Insulinetoediening Hervatten Mislukt" + "value" : "Insulinetoediening hervatten mislukt" } }, "pl" : { @@ -19146,6 +19326,12 @@ "value" : "Favorittmat" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favoriete voedingsmiddelen" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -19500,7 +19686,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Los nu op door Meldingen, Kritieke Meldingen en Tijdgevoelige Meldingen AAN te zetten." + "value" : "Los nu op door Meldingen, Kritieke meldingen en Tijdgevoelige meldingen AAN te zetten." } }, "pl" : { @@ -19806,6 +19992,12 @@ "value" : "Av sikkerhetshensyn bør du tillate at kritiske varsler, tidssensitive varsler og varslingstillatelser (ikke-kritiske varsler) på enheten din fortsetter å bruke %1$@ og ikke kan slå av individuelle alarmer." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voor je veiligheid moet je Kritieke meldingen, Tijdgevoelige meldingen en Meldingen (niet-kritieke waarschuwingen) op je apparaat toestaan om %1$@ te blijven gebruiken. Je kunt afzonderlijke alarmen niet uitschakelen." + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -19962,7 +20154,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Voorspelde Glucose" + "value" : "Voorspelde glucose" } }, "pl" : { @@ -20030,6 +20222,12 @@ "value" : "Ofte stilte spørsmål om varsler" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veelgestelde vragen over waarschuwingen" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20223,7 +20421,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Krijg hulp bij Toestemming Meldingen" + "value" : "Krijg hulp bij toestemming meldingen" } }, "pl" : { @@ -20422,6 +20620,12 @@ "value" : "Partiell anvendelse basert på glukosenivå" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucose-afhankelijke gedeeltelijke toediening" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20728,7 +20932,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Glucosegegevens Nu Beschikbaar" + "value" : "Glucosegegevens nu beschikbaar" } }, "pl" : { @@ -20802,6 +21006,12 @@ "value" : "Glukoseeffekt av å suspendere insulintilførsel" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucose-effect van onderbroken insulinetoediening" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -20870,7 +21080,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Glucose-invoer Buiten Bereik" + "value" : "Glucose-invoer buiten bereik" } }, "pl" : { @@ -21139,6 +21349,12 @@ "value" : "MASKINVARELYDER" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "GELUIDEN VAN APPARATUUR" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21197,6 +21413,12 @@ "value" : "Høyt glukosenivå" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoge glucose" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21255,6 +21477,12 @@ "value" : "Hvordan kan jeg dempe ikke-kritiske varsler?" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoe demp ik niet-kritieke waarschuwingen?" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21313,6 +21541,12 @@ "value" : "Hvordan kan jeg deaktivere kun tidssensitive og ikke-kritiske varsler?" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoe demp ik alleen tijdgevoelige en niet-kritieke waarschuwingen?" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21372,6 +21606,12 @@ "value" : "Hvordan kan jeg midlertidig dempe alle %1$@-applydene?" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoe demp ik tijdelijk alle geluiden van de %1$@-app?" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21573,6 +21813,12 @@ "value" : "Hvis iOS Focus Mode er PÅ og Mute Alerts er AV, vil kritiske varsler fortsatt bli levert og ikke-kritiske varsler vil bli dempet inntil %1$@ er lagt til i hver Focus Mode som en tillatt app." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Als een iOS-focusmodus AAN staat en Waarschuwingen dempen UIT, worden Kritieke meldingen nog steeds afgeleverd. Niet-kritieke waarschuwingen blijven stil totdat %1$@ in elke focusmodus als toegestane app is toegevoegd." + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21709,6 +21955,12 @@ "value" : "I fremtidige versjoner av Loop kan disse eksperimentene endres, ende opp som standarddeler av Loop-algoritmen eller fjernes helt fra Loop. Følg med i Loop Zulip-chatten for å holde deg informert om eventuelle endringer i disse funksjonene." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "In toekomstige versies van Loop kunnen deze experimenten veranderen, een vast onderdeel van het Loop-algoritme worden of helemaal uit Loop verdwijnen. Volg de Loop Zulip-chat om op de hoogte te blijven van mogelijke wijzigingen aan deze functies." + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -21875,6 +22127,12 @@ "value" : "Informasjon" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Informatie" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -22074,7 +22332,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Opgenomen Insuline (E) x Insulinegevoeligheid (%1$@/E)" + "value" : "Opgenomen insuline (E) x insulinegevoeligheid (%1$@/E)" } }, "pl" : { @@ -22926,7 +23184,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Insuline Onderbroken" + "value" : "Insulinetoediening onderbroken" } }, "pl" : { @@ -23107,6 +23365,12 @@ "value" : "Integrert retrospektiv korreksjon" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Integrale retrospectieve correctie" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23160,6 +23424,12 @@ "value" : "Integral Retrospective Correction (IRC) er en utvidelse av standardalgoritmekomponenten Retrospective Correction (RC) i Loop, som justerer prognosen basert på historikken for avvik mellom forventede og faktiske glukosenivåer.\n\nI motsetning til RC, som ser på avvik i løpet av de siste 30 minuttene, summerer IRC avvikene over tid. Fortsatte positive avvik over tid vil derfor føre til økt dosering. Hvis avvikene er negative over tid, vil Loop redusere doseringen ytterligere." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Integrale retrospectieve correctie (IRC) is een uitbreiding van het standaardonderdeel Retrospectieve correctie (RC) van het Loop-algoritme. RC past de voorspelling aan op basis van eerdere verschillen tussen voorspelde en werkelijke glucosewaarden.\n\nRC kijkt alleen naar verschillen in de laatste 30 minuten. Bij IRC tellen de verschillen in de loop van de tijd op. Blijven de verschillen langere tijd positief, dan doseert Loop meer. Zijn ze langere tijd negatief, dan doseert Loop nog minder." + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23264,7 +23534,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "中断的 %1$@:%3$@ %4$@ 的 %2$@" + "value" : "中断的 %1$@:计划剂量:%3$@ %4$@, 实际剂量: %2$@" } } } @@ -23302,6 +23572,12 @@ "value" : "Ugyldig absorpsjonstid: %1$@ timer" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ongeldige opnametijd: %1$@ uur" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23650,7 +23926,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Ongeldige Toekomstige Glucose" + "value" : "Ongeldige toekomstige glucose" } }, "pl" : { @@ -23794,6 +24070,12 @@ "value" : "iOS Critical Alerts og Time Sensitive Alerts er typer Apple-varsler. De brukes ved hendelser med høy prioritet. Noen eksempler er:" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kritieke meldingen en Tijdgevoelige meldingen zijn soorten meldingen van Apple in iOS. Ze worden gebruikt voor belangrijke gebeurtenissen. Enkele voorbeelden:" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -23852,6 +24134,12 @@ "value" : "IOS-FOKUSMODUSER" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "FOCUSMODI VAN IOS" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -24122,7 +24410,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Grote Maaltijd Ingevoerd" + "value" : "Grote maaltijd ingevoerd" } }, "pl" : { @@ -24337,7 +24625,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Meer Informatie" + "value" : "Meer informatie" } }, "pl" : { @@ -24497,7 +24785,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Live Activiteit" + "value" : "Live activiteit" } }, "ro" : { @@ -24655,6 +24943,12 @@ "value" : "Låseskjerm / Dynamic Island / CarPlay" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toegangsscherm / Dynamic Island / CarPlay" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -24717,7 +25011,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Registreer Dosis" + "value" : "Registreer dosis" } }, "pl" : { @@ -24806,7 +25100,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geregistreerde Insulinedosis" + "value" : "Geregistreerde insulinedosis" } }, "pl" : { @@ -25021,7 +25315,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Loop is Vastgelopen" + "value" : "Loop is vastgelopen" } }, "pl" : { @@ -25223,7 +25517,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Loop heeft een probleem met je Bluetoothinstellingen gedetecteerd en zal niet goed werken totdat Bluetooth wordt ingeschakeld. Je ontvangt geen glucosemetingen en je kunt niet bolussen." + "value" : "Loop heeft een probleem met je Bluetooth-instellingen gedetecteerd en zal niet goed werken totdat Bluetooth wordt ingeschakeld. Je ontvangt geen glucosemetingen en je kunt niet bolussen." } }, "pl" : { @@ -25481,6 +25775,12 @@ "value" : "Loop gir normalt 40 % av det forventede insulinbehovet ditt i hver doseringssyklus. Når eksperimentet Glukosebasert delvis tilførsel er aktivert, vil Loop variere prosentandelen av anbefalt bolus som gis i hver syklus med glukosenivået.\n\nI nærheten av korreksjonsområdet vil den bruke 20 % (på samme måte som Temp Basal), og gradvis øke til maksimalt 80 % ved høyt glukosenivå (200 mg/dL, 11,1 mmol/L). Vær oppmerksom på at ved raskt stigende glukose, for eksempel etter et uanmeldt måltid, kan denne funksjonen, kombinert med hastighet og retrospektive korreksjonseffekter, resultere i en større dose enn det ISF-en din ville ha krevd." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop geeft normaal gesproken per doseercyclus 40% van je voorspelde insulinebehoefte.\n\nAls het experiment Glucose-afhankelijke gedeeltelijke toediening aan staat, past Loop het percentage van de aanbevolen bolus dat per cyclus wordt toegediend aan op je glucosewaarde.\n\nIn de buurt van je correctiebereik is dat 20% (vergelijkbaar met Tijdelijke basaal). Bij hoge glucose loopt het geleidelijk op tot maximaal 80% (200 mg/dL, 11,1 mmol/L).\n\nLet op: bij snel stijgende glucose, zoals na een niet-ingevoerde maaltijd, kan deze functie samen met de effecten van snelheid en retrospectieve correctie een grotere dosis geven dan je ISF zou aangeven." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -25810,7 +26110,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Lage Glucose" + "value" : "Lage glucose" } }, "pl" : { @@ -25893,7 +26193,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Beheer Toestemming in Instellingen" + "value" : "Beheer toestemming in Instellingen" } }, "pl" : { @@ -25961,6 +26261,12 @@ "value" : "Administrere varsler" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Waarschuwingen beheren" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -26035,7 +26341,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Doseer Handmatig: %1$@ %2$@" + "value" : "Doseer handmatig: %1$@ %2$@" } }, "pl" : { @@ -26118,7 +26424,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Maximale Basaalsnelheid Per Uur" + "value" : "Maximale basaalstand per uur" } }, "pl" : { @@ -26213,7 +26519,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Maximale Bolus" + "value" : "Maximale bolus" } }, "pl" : { @@ -26314,7 +26620,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Maximale Bolus Overschreden" + "value" : "Maximale bolus overschreden" } }, "pl" : { @@ -26403,7 +26709,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Maaltijd Bolus" + "value" : "Maaltijd bolus" } }, "pl" : { @@ -26611,7 +26917,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gemiste Maaltijdmeldingen" + "value" : "Gemiste maaltijdmeldingen" } }, "pl" : { @@ -26994,6 +27300,12 @@ "value" : "Modus" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modus" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -27187,7 +27499,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Meer Informatie" + "value" : "Meer informatie" } }, "pl" : { @@ -27288,7 +27600,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Alle Waarschuwingen Dempen" + "value" : "Alle waarschuwingen dempen" } }, "pl" : { @@ -27356,6 +27668,12 @@ "value" : "Slå av alle varsler midlertidig" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle waarschuwingen tijdelijk dempen" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27568,7 +27886,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Aandacht Nodig" + "value" : "Aandacht nodig" } }, "pl" : { @@ -27719,6 +28037,12 @@ "value" : "Ny favorittmat" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nieuw favoriet voedingsmiddel" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -27904,6 +28228,12 @@ "value" : "Ingen varsler eller alarmer vil høres mens de er dempet. Velg hvor lenge du ønsker å dempe lyden." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zolang dempen aan staat, klinken er geen waarschuwingen of alarmen. Kies hoe lang je wilt dempen." + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -27978,7 +28308,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen Bolus Aanbevolen" + "value" : "Geen bolus aanbevolen" } }, "pl" : { @@ -28192,7 +28522,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen Maximale Bolus Geconfigureerd" + "value" : "Geen maximale bolus geconfigureerd" } }, "pl" : { @@ -28287,7 +28617,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen Pomp Geconfigureerd" + "value" : "Geen pomp geconfigureerd" } }, "pl" : { @@ -28382,7 +28712,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen Recente Glucose" + "value" : "Geen recente glucose" } }, "pl" : { @@ -28477,7 +28807,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen Recente Glucosegegevens" + "value" : "Geen recente glucosegegevens" } }, "pl" : { @@ -28572,7 +28902,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen Recente Pompgegevens" + "value" : "Geen recente pompgegevens" } }, "pl" : { @@ -28875,7 +29205,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Afleveren Meldingen" + "value" : "Afleveren meldingen" } }, "pl" : { @@ -28958,7 +29288,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "De aflevering van meldingen in de instellingen van je telefoon staat op Gepland Overzicht.\n\nOm vertraging bij het ontvangen van meldingen van %1$@ te voorkomen, raden we je aan om de aflevering van meldingen in te stellen op Onmiddellijk Afleveren." + "value" : "De aflevering van meldingen in de instellingen van je telefoon staat op Gepland overzicht.\n\nOm vertraging bij het ontvangen van meldingen van %1$@ te voorkomen, raden we je aan om de aflevering van meldingen in te stellen op Onmiddellijk afleveren." } }, "pl" : { @@ -29124,7 +29454,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Meldingen Vertraagd" + "value" : "Meldingen vertraagd" } }, "pl" : { @@ -29290,7 +29620,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Meldingen geven je belangrijke %1$@ appinformatie zonder dat je de app hoeft te openen.\n\nHoud deze instellingen AAN in je telefooninstellingen om ervoor te zorgen dat %1$@ je Meldingen, Kritieke Meldingen en Tijdgevoelige Meldingen ontvangt." + "value" : "Meldingen geven je belangrijke %1$@ appinformatie zonder dat je de app hoeft te openen.\n\nHoud deze instellingen AAN in je telefooninstellingen om ervoor te zorgen dat %1$@ je meldingen, Kritieke meldingen en Tijdgevoelige meldingen ontvangt." } }, "pl" : { @@ -29587,7 +29917,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Ok" + "value" : "OK" } }, "pl" : { @@ -29850,7 +30180,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Override Programma's" + "value" : "Override programma's" } }, "pl" : { @@ -29945,7 +30275,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Mogelijk Gemiste Maaltijd" + "value" : "Mogelijk gemiste maaltijd" } }, "pl" : { @@ -30046,7 +30376,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Pre-Meal Doelen" + "value" : "Pre-Meal doelen" } }, "pl" : { @@ -30692,6 +31022,12 @@ "value" : "prediksjon-beskrivelse-integral-retrospektiv-korreksjon" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Integraal effect: %1$@\nTotaal glucose-effect: %2$@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -30898,7 +31234,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Kritieke Gebeurtenislogboek Voorbereiden" + "value" : "Kritieke gebeurtenislogboek voorbereiden" } }, "pl" : { @@ -30993,7 +31329,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Profiel Vervaldatum" + "value" : "Profiel vervaldatum" } }, "pl" : { @@ -31165,7 +31501,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Profiel Verloopt Binnenkort" + "value" : "Profiel verloopt binnenkort" } }, "pl" : { @@ -31399,7 +31735,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Pompbatterij Bijna Leeg" + "value" : "Pompbatterij bijna leeg" } }, "pl" : { @@ -31719,7 +32055,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Pomp Verlopen" + "value" : "Pomp verlopen" } }, "pl" : { @@ -31814,7 +32150,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Pomp Manager" + "value" : "Pomp manager" } }, "pl" : { @@ -31915,7 +32251,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Pompmanager Fout: %1$@" + "value" : "Pompmanager fout: %1$@" } }, "pl" : { @@ -32023,7 +32359,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Pompreservoir Leeg" + "value" : "Pompreservoir leeg" } }, "pl" : { @@ -32149,7 +32485,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Pompreservoir Bijna Leeg" + "value" : "Pompreservoir bijna leeg" } }, "pl" : { @@ -32275,7 +32611,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Pomp Onderbroken" + "value" : "Pomp onderbroken" } }, "pl" : { @@ -32382,7 +32718,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Pomp Onderbroken. Automatisch doseren is uitgeschakeld." + "value" : "Pomp onderbroken. Automatisch doseren is uitgeschakeld." } }, "pl" : { @@ -32622,7 +32958,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Snelwerkend - Volwassenen" + "value" : "Snelwerkend - volwassenen" } }, "pl" : { @@ -32748,7 +33084,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Snelwerkend - Kinderen" + "value" : "Snelwerkend - kinderen" } }, "pl" : { @@ -32993,7 +33329,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Aanbevolen Basaal" + "value" : "Aanbevolen basaalstand" } }, "pl" : { @@ -33106,7 +33442,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Aanbevolen Bolus" + "value" : "Aanbevolen bolus" } }, "pl" : { @@ -33195,7 +33531,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Aanbevolen Bolus Overschrijdt Maximale Bolus" + "value" : "Aanbevolen bolus overschrijdt maximale bolus" } }, "pl" : { @@ -33303,7 +33639,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Aanbevolen Bolus: %@ Eenheden" + "value" : "Aanbevolen bolus: %@ eenheden" } }, "pl" : { @@ -33404,7 +33740,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Remote Bolusinvoer: %@ E" + "value" : "Remote bolusinvoer: %@ E" } }, "pl" : { @@ -33487,7 +33823,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Remote Koolhydraatinvoer: %d gram" + "value" : "Remote koolhydraatinvoer: %d gram" } }, "pl" : { @@ -33679,6 +34015,12 @@ "value" : "Reservoaret er tomt" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reservoir leeg" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -33771,7 +34113,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Retroperspectieve Correctie" + "value" : "Retrospectieve correctie" } }, "pl" : { @@ -33896,7 +34238,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Opnieuw Proberen" + "value" : "Opnieuw proberen" } }, "pl" : { @@ -34099,6 +34441,12 @@ "value" : "Lagre og gi bolus" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koolhydraten opslaan en toedienen" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -34167,7 +34515,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Opslaan zonder Bolussen" + "value" : "Opslaan zonder bolussen" } }, "pl" : { @@ -34313,6 +34661,12 @@ "value" : "Velg ut opsjonene til låseskjermvisning" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kies weergaveopties voor toegangsscherm" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -34417,6 +34771,12 @@ "value" : "Sensor mislyktes" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sensorfout" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -34735,7 +35095,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Installatie Onvolledig" + "value" : "Installatie onvolledig" } }, "pl" : { @@ -34943,7 +35303,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Eenvoudige Boluscalculator" + "value" : "Eenvoudige boluscalculator" } }, "pl" : { @@ -35038,7 +35398,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Eenvoudige Maaltijdcalculator" + "value" : "Eenvoudige maaltijdcalculator" } }, "pl" : { @@ -35681,7 +36041,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bolus Starten" + "value" : "Bolus starten" } }, "pl" : { @@ -35988,6 +36348,12 @@ "value" : "Suspensjon av insulintilførsel" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Onderbreking van insulinetoediening" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -36293,7 +36659,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Tik hier om een Service in te stellen" + "value" : "Tik hier om een service in te stellen" } }, "pl" : { @@ -36388,7 +36754,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Tik voor Toevoegen" + "value" : "Tik voor toevoegen" } }, "pl" : { @@ -36495,7 +36861,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Tik voor Hervatten" + "value" : "Tik voor hervatten" } }, "pl" : { @@ -36602,7 +36968,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Tik voor Stoppen" + "value" : "Tik voor stoppen" } }, "pl" : { @@ -36676,6 +37042,12 @@ "value" : "Trykk for å dempe varsler" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tik om dempen op te heffen" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -36729,6 +37101,12 @@ "value" : "Trykk på Slå av lyd for å gjenoppta lyden for varsler og alarmer." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tik op Dempen opheffen om het geluid van je waarschuwingen en alarmen weer aan te zetten." + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -36850,7 +37228,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "TestFlight Verloopdatum" + "value" : "TestFlight verloopdatum" } }, "pl" : { @@ -37140,7 +37518,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Het bolusdoseeralgoritme gebruikt een meer conservatieve schatting van de verwachte bloedglucose dan wat wordt gebruikt om je basaalsnelheid aan te passen.\n\nHierdoor kan je voorspelde bloedglucose na een bolus nog steeds hoger zijn dan je streefbereik." + "value" : "Het bolusdoseeralgoritme gebruikt een meer conservatieve schatting van de verwachte bloedglucose dan wat wordt gebruikt om je basaalstand aan te passen.\n\nHierdoor kan je voorspelde bloedglucose na een bolus nog steeds hoger zijn dan je streefbereik." } }, "pl" : { @@ -37730,7 +38108,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "De maximale bolushoeveelheid is %@ Eenheden" + "value" : "De maximale bolushoeveelheid is %@ eenheden" } }, "pl" : { @@ -37997,7 +38375,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Dit is de ontwikkelingsversie van Loop, samengesteld op basis van de dev-branch. Updates op deze branch kunnen nieuwe, nog ongeteste functies bevatten en mogelijk onveilig zijn. Als je geen tester bent, gebruik deze branch dan alsjeblieft niet en schakel over naar de main-branch." + "value" : "Dit is de ontwikkelversie van Loop, gebouwd vanuit de dev-branch. Updates op deze branch kunnen nieuwe, nog niet geteste functies bevatten en zijn mogelijk niet veilig. Test je niet mee? Gebruik deze branch dan niet en stap over op de 'main' branch." } }, "ro" : { @@ -38047,6 +38425,12 @@ "value" : "Dette alternativet gjelder bare når Loops doseringsstrategi er satt til Automatisk bolus." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deze optie werkt alleen als de doseerstrategie van Loop op Automatisch bolussen staat." + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -38105,6 +38489,12 @@ "value" : "Tidssensitive varsler" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tijdgevoelige meldingen" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -38179,7 +38569,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Tijdgevoelige Meldingen" + "value" : "Tijdgevoelige meldingen" } }, "pl" : { @@ -38246,6 +38636,12 @@ "value" : "Svakt batteri i senderen" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Batterij zender bijna leeg" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -38326,7 +38722,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Probeer Opnieuw" + "value" : "Probeer opnieuw" } }, "pl" : { @@ -38400,6 +38796,12 @@ "value" : "Slå av volumet på iOS-enheten eller legg til %1$@ som en tillatt app i hver fokusmodus. Tidssensitive og kritiske varsler vil fortsatt høres, men ikke-kritiske varsler blir dempet." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zet het volume van je iOS-apparaat uit of voeg %1$@ in elke focusmodus toe als toegestane app. Tijdgevoelige en Kritieke meldingen maken dan nog steeds geluid, maar niet-kritieke waarschuwingen zijn stil." + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -38694,7 +39096,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Kan Waarschuwing Niet Wissen" + "value" : "Kan waarschuwing niet wissen" } }, "pl" : { @@ -38783,7 +39185,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Kan Pomp Niet Bereiken" + "value" : "Kan pomp niet bereiken" } }, "pl" : { @@ -38878,7 +39280,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Kan Koolhydraatinvoer Niet Opslaan" + "value" : "Kan koolhydraatinvoer niet opslaan" } }, "pl" : { @@ -38973,7 +39375,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Kan Handmatige Glucose-invoer Niet Opslaan" + "value" : "Kan handmatige glucose-invoer niet opslaan" } }, "pl" : { @@ -39294,7 +39696,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Onbekende Fout: %1$@" + "value" : "Onbekende fout: %1$@" } }, "pl" : { @@ -39528,6 +39930,12 @@ "value" : "Oppheve demping" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dempen opheffen" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -39587,6 +39995,12 @@ "value" : "Oppheve demping av varsler?" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dempen opheffen?" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -39646,6 +40060,12 @@ "value" : "Varslingstjeneste som ikke støttes: %1$@" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niet-ondersteunde meldingsservice: %1$@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -39824,6 +40244,12 @@ "value" : "Inntil %1$@" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tot %1$@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -40072,6 +40498,12 @@ "value" : "Øyeblikkelig lav" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Urgent laag" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -40120,6 +40552,12 @@ "value" : "Bruk BG fargelegging" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kleuren voor glucose gebruiken" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -40188,7 +40626,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gebruik Pre-Meal Programma" + "value" : "Gebruik Pre-Meal programma" } }, "pl" : { @@ -40262,6 +40700,12 @@ "value" : "Bruk funksjonen Demp varsler. Med denne funksjonen kan du midlertidig dempe alle varsler og alarmer via %1$@-appen, inkludert kritiske varsler og tidssensitive varsler." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gebruik de functie Waarschuwingen dempen. Daarmee demp je tijdelijk al je waarschuwingen en alarmen via de %1$@-app, ook Kritieke en Tijdgevoelige meldingen." + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -40361,7 +40805,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gebruik Trainingsglucosedoelen" + "value" : "Gebruik trainingsglucosedoelen" } }, "pl" : { @@ -40468,7 +40912,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gebruik Trainingsprogramma" + "value" : "Gebruik trainingsprogramma" } }, "pl" : { @@ -40791,6 +41235,12 @@ "value" : "Hva er eksempler på kritiske og tidssensitive varsler?" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wat zijn voorbeelden van kritieke en tijdgevoelige waarschuwingen?" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -40985,7 +41435,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Wanneer de huidige of voorspelde glucose onder de onderbrekingsdrempel ligt, zal Loop geen bolus aanbevelen en zal Loop altijd een tijdelijke basaalsnelheid van 0 eenheden per uur aanbevelen." + "value" : "Wanneer de huidige of voorspelde glucose onder de onderbrekingsdrempel ligt, zal Loop geen bolus aanbevelen en zal Loop altijd een tijdelijke basaalsstand van 0 eenheden per uur aanbevelen." } }, "pl" : { @@ -41168,7 +41618,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Als de Gesloten Loop modus is uitgeschakeld, gebruikt de app een vereenvoudigde boluscalculator zoals bij een gewone pomp." + "value" : "Als Actieve loop uit staat, gebruikt de app een vereenvoudigde boluscalculator, zoals bij een gewone pomp." } }, "pl" : { @@ -41242,6 +41692,12 @@ "value" : "Når lydløse varsler er på, vil alle varsler fra %1$@-appen, inkludert kritiske og tidssensitive varsler, midlertidig vises uten lyd og kun vibrere." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zolang dempen aan staat, worden alle waarschuwingen van je %1$@-app, ook Kritieke en Tijdgevoelige meldingen, tijdelijk zonder geluid getoond. Ze trillen alleen." + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -41300,6 +41756,12 @@ "value" : "Selv om dempede varsler er aktivert, kan insulinpumpen og CGM-maskinvaren fortsatt avgi lyd." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zolang dempen aan staat, kunnen je insulinepomp en CGM zelf nog steeds geluid maken." + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -41359,6 +41821,12 @@ "value" : "Det oppstod en feil da du prøvde å starte %1$@ på nytt.\n\n%2$@" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Er is een fout opgetreden bij het herstarten van %1$@.\n\n%2$@" + } + }, "pl" : { "stringUnit" : { "state" : "translated", @@ -41558,7 +42026,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Tijdelijk Trainingsprogramma staat meer dan 24 uur ingeschakeld. Zorg ervoor dat je deze nog steeds wilt inschakelen of schakel deze uit in de app." + "value" : "Tijdelijk trainingsprogramma staat meer dan 24 uur ingeschakeld. Zorg ervoor dat je deze nog steeds wilt inschakelen of schakel deze uit in de app." } }, "pl" : { @@ -41653,7 +42121,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Tijdelijk Trainingsprogramma Nog Steeds Aan" + "value" : "Tijdelijk trainingsprogramma nog steeds aan" } }, "pl" : { @@ -41831,7 +42299,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Mogelijk ontvangt je geen geluids-, visuele of trillingswaarschuwingen met betrekking tot kritieke veiligheidsinformatie. \n\nOm het probleem op te lossen, tikt op 'Instellingen' en zorg ervoor dat Meldingen, Kritieke Meldingen en Tijdgevoelige Meldingen AAN staan." + "value" : "Mogelijk ontvang je geen geluids-, visuele of trillingswaarschuwingen met betrekking tot kritieke veiligheidsinformatie. \n\nOm het probleem op te lossen, tik op 'Instellingen' en zorg ervoor dat Meldingen, Kritieke meldingen en Tijdgevoelige meldingen AAN staan." } }, "pl" : { @@ -41914,7 +42382,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Je %1$@'s tijd is veranderd. %2$@ heeft nauwkeurige tijdsregistraties nodig om voorspellingen te doen over je glucose en dienovereenkomstig je insuline aan te passen.\n\nControleer in je %1$@ Instellingen (Algemeen / Datum & Tijd) en controleer of 'Automatisch Instellen' is INGESCHAKKELD. Als dit niet wordt opgelost, kan dit leiden tot ernstig te weinig toediening of tot ernstige overmatige toediening van insuline." + "value" : "Je %1$@'s tijd is veranderd. %2$@ heeft nauwkeurige tijdsregistraties nodig om voorspellingen te doen over je glucose en dienovereenkomstig je insuline aan te passen.\n\nControleer in je %1$@ Instellingen (Algemeen > Datum en tijd) en controleer of 'Automatisch instellen' is INGESCHAKELD. Als dit niet wordt opgelost, kan dit leiden tot ernstig te weinig toediening of tot ernstige overmatige toediening van insuline." } }, "pl" : { @@ -42602,7 +43070,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Je pomp geeft een handmatig ingestelde tijdelijke basaalsnelheid af." + "value" : "Je pomp geeft een handmatig ingestelde tijdelijke basaalstand af." } }, "pl" : { diff --git a/Loop/mul.lproj/Main.xcstrings b/Loop/mul.lproj/Main.xcstrings index b0c0479ee4..a809fb9991 100644 --- a/Loop/mul.lproj/Main.xcstrings +++ b/Loop/mul.lproj/Main.xcstrings @@ -608,7 +608,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Etiket" + "value" : "Label" } }, "pl" : { @@ -866,7 +866,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "g Totaal" + "value" : "g totaal" } }, "pl" : { @@ -1130,7 +1130,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Etiket" + "value" : "Label" } }, "pl" : { @@ -2042,7 +2042,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Aanbevolen Basaal" + "value" : "Aanbevolen basaalstand" } }, "pl" : { @@ -2300,7 +2300,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Etiket" + "value" : "Label" } }, "pl" : { @@ -2689,7 +2689,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "g Actieve Kh" + "value" : "g actieve kh" } }, "pl" : { @@ -2959,7 +2959,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Etiket" + "value" : "Label" } }, "pl" : { @@ -3217,7 +3217,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Aanbevolen Basaal" + "value" : "Aanbevolen basaalstand" } }, "pl" : { diff --git a/LoopCore/Localizable.xcstrings b/LoopCore/Localizable.xcstrings index 8466837d54..85e409d185 100644 --- a/LoopCore/Localizable.xcstrings +++ b/LoopCore/Localizable.xcstrings @@ -152,7 +152,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Actieve Koolhydraten" + "value" : "Actieve koolhydraten" } }, "ro" : { @@ -190,6 +190,12 @@ "value" : "Aktive karbohydrater (COB)" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actieve koolhydraten (COB)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -230,7 +236,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Actieve Koolhydraten" + "value" : "Actieve koolhydraten" } }, "ro" : { @@ -273,7 +279,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Actieve Insuline" + "value" : "Actieve insuline" } }, "ro" : { @@ -311,6 +317,12 @@ "value" : "Aktiv insulin (IOB)" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actieve insuline (IOB)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -346,6 +358,12 @@ "value" : "kl." } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "om" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -408,7 +426,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Automatisch Bolussen" + "value" : "Automatisch bolussen" } }, "pl" : { @@ -511,6 +529,12 @@ "value" : "COB" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -592,7 +616,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Huidige Glucose" + "value" : "Huidige glucose" } }, "ro" : { @@ -630,6 +654,12 @@ "value" : "Nåværende Glukose (Verdi og Pil)" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Huidige glucose (waarde en pijl)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -665,6 +695,12 @@ "value" : "Delta" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -700,6 +736,12 @@ "value" : "Delta Glukose (Delta)" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucoseverandering (Delta)" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -819,6 +861,12 @@ "value" : "Forutsagt glukoseverdi (eventuell BG)" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voorspelde glucose" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -980,6 +1028,12 @@ "value" : "Felt og rad" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grafiek en rij" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -1015,6 +1069,12 @@ "value" : "Netto basaldose (basal)" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Relatieve basaalstand" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -1050,6 +1110,12 @@ "value" : "Kun rad" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alleen rij" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -1155,7 +1221,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Alleen Tijdelijk Basaal" + "value" : "Alleen tijdelijk basaal" } }, "pl" : { @@ -1219,6 +1285,12 @@ "value" : "Oppdatert" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bijgewerkt" + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -1297,6 +1369,12 @@ "value" : "Oppdatert sist" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bijgewerkt om" + } + }, "ro" : { "stringUnit" : { "state" : "translated", diff --git a/LoopUI/Localizable.xcstrings b/LoopUI/Localizable.xcstrings index e6a1a5bc0c..cfa6d1fe73 100644 --- a/LoopUI/Localizable.xcstrings +++ b/LoopUI/Localizable.xcstrings @@ -43,7 +43,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "\n%1$@\n\n%2$@ werkt met Gesloten Loop in de AAN stand" + "value" : "\n%1$@\n\n%2$@ werkt met Actieve loop AAN" } }, "pl" : { @@ -274,7 +274,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "\n%1$@ werkt met Gesloten Loop in de UIT stand. Je pomp en CGM blijven werken, maar de app past de dosering niet automatisch aan.\n\n%2$@" + "value" : "\n%1$@ werkt met Actieve loop UIT. Je pomp en CGM blijven werken, maar de app past de dosering niet automatisch aan.\n\n%2$@" } }, "pl" : { @@ -1476,7 +1476,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gesloten loop" + "value" : "Actieve loop" } }, "pl" : { @@ -1577,7 +1577,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gesloten Loop UIT" + "value" : "Actieve loop UIT" } }, "pl" : { @@ -1666,7 +1666,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gesloten Loop AAN" + "value" : "Actieve loop AAN" } }, "pl" : { @@ -3022,7 +3022,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Open loop" + "value" : "Inactieve loop" } }, "pl" : { @@ -3243,7 +3243,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Tik op Instellingen om Gesloten Loop AAN te zetten als je wilt dat de app je insulinetoediening automatiseert." + "value" : "Tik op Instellingen om Actieve loop AAN te zetten als je wilt dat de app je insulinetoediening automatiseert." } }, "pl" : { diff --git a/WatchApp Extension/Localizable.xcstrings b/WatchApp Extension/Localizable.xcstrings index 4c26d66fe7..e487759cf6 100644 --- a/WatchApp Extension/Localizable.xcstrings +++ b/WatchApp Extension/Localizable.xcstrings @@ -488,7 +488,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Actieve Koolhydraten" + "value" : "Actieve koolhydraten" } }, "pl" : { @@ -613,7 +613,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Actieve Insuline" + "value" : "Actieve insuline" } }, "pl" : { @@ -738,7 +738,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Kh. Inv. Toevoegen" + "value" : "Koolhydraten toevoegen" } }, "pl" : { @@ -988,7 +988,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bolus Mislukt" + "value" : "Bolus mislukt" } }, "pl" : { @@ -1095,7 +1095,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Aanbevolen Bolus Bijgewerkt" + "value" : "Bolusadvies bijgewerkt" } }, "pl" : { @@ -1202,7 +1202,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Ga Verder" + "value" : "Ga verder" } }, "pl" : { @@ -2154,7 +2154,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Netto Basaalsnelheid" + "value" : "Netto basaalstand" } }, "pl" : { @@ -2385,7 +2385,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Ok" + "value" : "OK" } }, "pl" : { @@ -3720,7 +3720,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Opslaan & Bolussen" + "value" : "Opslaan & bolussen" } }, "pl" : { @@ -3827,7 +3827,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Versturen Mislukt" + "value" : "Versturen mislukt" } }, "pl" : { diff --git a/WatchApp/mul.lproj/Interface.xcstrings b/WatchApp/mul.lproj/Interface.xcstrings index 2de01fea8b..57c35ea8cb 100644 --- a/WatchApp/mul.lproj/Interface.xcstrings +++ b/WatchApp/mul.lproj/Interface.xcstrings @@ -199,7 +199,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Totaal Koolhydraten" + "value" : "Totaal koolhydraten" } }, "pl" : { @@ -839,7 +839,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bolus Mislukt" + "value" : "Bolus mislukt" } }, "pl" : { @@ -1738,7 +1738,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Etiket" + "value" : "Label" } }, "pl" : { @@ -2511,7 +2511,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Etiket" + "value" : "Label" } }, "pl" : { From 2df7179c966e5ee5feb05cc9104853d42308dbdb Mon Sep 17 00:00:00 2001 From: Marion Barker <19607791+marionbarker@users.noreply.github.com> Date: Sat, 26 Sep 2026 18:03:30 -0700 Subject: [PATCH 17/26] Add missing translations string, update WatchApp.xcscheme for Xcode 27 (#2536) --- Loop/Localizable.xcstrings | 5 ++++- Loop/Views/SettingsView.swift | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index b133b77b79..4d4d0eb7ae 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -19300,7 +19300,7 @@ } }, "Favorite Foods" : { - "comment" : "Title for Favorite Foods view", + "comment" : "Label for Favorite Foods icon in Settings\nTitle for Favorite Foods view", "localizations" : { "de" : { "stringUnit" : { @@ -35439,6 +35439,9 @@ } } }, + "Simplify Carb Entry" : { + "comment" : "Subheading for Favorite Foods in Settings" + }, "since %@" : { "comment" : "Format fragment for a start time", "localizations" : { diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 4d65f5b2ca..5575fa8c33 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -365,8 +365,8 @@ extension SettingsView { LargeButton(action: { sheet = .favoriteFoods }, includeArrow: true, imageView: Image("Favorite Foods Icon").renderingMode(.template).foregroundColor(carbTintColor), - label: "Favorite Foods", - descriptiveText: "Simplify Carb Entry") + label: NSLocalizedString("Favorite Foods", comment: "Label for Favorite Foods icon in Settings"), + descriptiveText: NSLocalizedString("Simplify Carb Entry", comment: "Subheading for Favorite Foods in Settings")) } } From 836dc5810163abf03fa7fff24b53f591a6a67014 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Tue, 29 Sep 2026 12:48:04 -0500 Subject: [PATCH 18/26] Don't ask to confirm a Loop reset before launch has completed (#2541) * Don't ask to confirm a Loop reset before launch has completed resumeLaunch() called askUserToConfirmLoopReset() unconditionally, but resetLoopManager is only created in launchManagers(). When the app is launched before the device has been unlocked after a reboot (a Bluetooth state-restoration launch), checkProtectedDataAvailable() defers the launch and the call traps on the nil manager: LoopAppManager.askUserToConfirmLoopReset() + 164 LoopAppManager.resumeLaunch() + 260 LoopAppManager.launch() + 412 SceneDelegate.scene(_:willConnectTo:options:) with wasUnlockedSinceBoot=0 and uptime 19s. Loop then stays dead until the user opens it. Only ask once the launch has completed; every re-entry into resumeLaunch() ends here, so the prompt is not lost. * Hold an early APNs registration result until SettingsManager exists initialize() registers for remote notifications before the launch is gated on protected data, so the token can arrive while the launch is still deferred and settingsManager is nil. Keep the result and deliver it once launchManagers() has created the manager. --- Loop/Managers/LoopAppManager.swift | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 04d561a0d3..3971e8c8c8 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -79,6 +79,7 @@ class LoopAppManager: NSObject { private var alertPermissionsChecker: AlertPermissionsChecker! private var supportManager: SupportManager! private var settingsManager: SettingsManager! + private var pendingRemoteNotificationRegistration: Result? private var loggingServicesManager = LoggingServicesManager() private var analyticsServicesManager = AnalyticsServicesManager() private(set) var testingScenariosManager: TestingScenariosManager? @@ -147,8 +148,10 @@ class LoopAppManager: NSObject { if state == .launchHomeScreen { launchHomeScreen() } - - askUserToConfirmLoopReset() + + if isLaunchComplete { + askUserToConfirmLoopReset() + } } private func checkProtectedDataAvailable() { @@ -194,6 +197,10 @@ class LoopAppManager: NSObject { settingsManager = SettingsManager(cacheStore: cacheStore, expireAfter: localCacheDuration, alertMuter: alertManager.alertMuter) + if let result = pendingRemoteNotificationRegistration { + pendingRemoteNotificationRegistration = nil + settingsManager.remoteNotificationRegistrationDidFinish(result) + } deviceDataManager = DeviceDataManager(pluginManager: pluginManager, alertManager: alertManager, @@ -343,6 +350,11 @@ class LoopAppManager: NSObject { if case .success(let token) = result { log.default("DeviceToken: %{public}@", token.hexadecimalString) } + // Can arrive while the launch is deferred until protected data is available + guard let settingsManager else { + pendingRemoteNotificationRegistration = result + return + } settingsManager.remoteNotificationRegistrationDidFinish(result) } From d6c211230efa513175f08782dfe98f07c9b38976 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 1 Oct 2026 15:24:06 -0500 Subject: [PATCH 19/26] Forward device manager analytics events to the analytics services (#2546) --- Loop/Managers/AnalyticsServicesManager.swift | 4 ++++ Loop/Managers/DeviceDataManager.swift | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Loop/Managers/AnalyticsServicesManager.swift b/Loop/Managers/AnalyticsServicesManager.swift index 808a34c81a..861489aba2 100644 --- a/Loop/Managers/AnalyticsServicesManager.swift +++ b/Loop/Managers/AnalyticsServicesManager.swift @@ -102,6 +102,10 @@ final class AnalyticsServicesManager { logEvent("Pump battery replacement", outOfSession: true) } + func deviceManagerDidRecordEvent(_ name: String, properties: [AnyHashable: Any]?) { + logEvent(name, withProperties: properties, outOfSession: true) + } + func reservoirWasRewound() { logEvent("Pump reservoir rewind", outOfSession: true) } diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 3d07128ebb..09ee0c11db 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -950,7 +950,11 @@ extension DeviceDataManager: DeviceManagerDelegate { func deviceManager(_ manager: DeviceManager, logEventForDeviceIdentifier deviceIdentifier: String?, type: DeviceLogEntryType, message: String, completion: ((Error?) -> Void)?) { deviceLog.log(managerIdentifier: manager.pluginIdentifier, deviceIdentifier: deviceIdentifier, type: type, message: message, completion: completion) } - + + func deviceManager(_ manager: DeviceManager, recordAnalyticsEvent name: String, properties: [AnyHashable: Any]?) { + analyticsServicesManager.deviceManagerDidRecordEvent(name, properties: properties) + } + var allowDebugFeatures: Bool { FeatureFlags.allowDebugFeatures // NOTE: DEBUG FEATURES - DEBUG AND TEST ONLY } From c30b5c89b1dc4151628f33f1384dd4796eaeeec2 Mon Sep 17 00:00:00 2001 From: Marion Barker <19607791+marionbarker@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:35:39 -0700 Subject: [PATCH 20/26] Updated translations from lokalise on Sat Oct 3 09:14:39 PDT 2026 (#2550) --- Loop.xcodeproj/project.pbxproj | 4 ++-- Loop/Localizable.xcstrings | 26 +++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index dfcb0b9516..8f8f9dce09 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -498,7 +498,6 @@ C1F00C60285A802A006302C5 /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = C1F00C5F285A802A006302C5 /* SwiftCharts */; }; C1F00C78285A8256006302C5 /* SwiftCharts in Embed Frameworks */ = {isa = PBXBuildFile; productRef = C1F00C5F285A802A006302C5 /* SwiftCharts */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; C1F2075C26D6F9B0007AB7EB /* AppExpirationAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */; }; - FADE0001000000000000DE02 /* DevelopmentBranchAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADE0001000000000000DE01 /* DevelopmentBranchAlerter.swift */; }; C1F7822627CC056900C0919A /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F7822527CC056900C0919A /* SettingsManager.swift */; }; C1F8B243223E73FD00DD66CF /* BolusProgressTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F8B1D122375E4200DD66CF /* BolusProgressTableViewCell.swift */; }; C1FB428C217806A400FAB378 /* StateColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FB428B217806A300FAB378 /* StateColorPalette.swift */; }; @@ -590,6 +589,7 @@ E9C58A7E24DB529A00487A17 /* dynamic_glucose_effect_partially_observed.json in Resources */ = {isa = PBXBuildFile; fileRef = E9C58A7924DB529A00487A17 /* dynamic_glucose_effect_partially_observed.json */; }; E9C58A7F24DB529A00487A17 /* counteraction_effect_falling_glucose.json in Resources */ = {isa = PBXBuildFile; fileRef = E9C58A7A24DB529A00487A17 /* counteraction_effect_falling_glucose.json */; }; E9C58A8024DB529A00487A17 /* insulin_effect.json in Resources */ = {isa = PBXBuildFile; fileRef = E9C58A7B24DB529A00487A17 /* insulin_effect.json */; }; + FADE0001000000000000DE02 /* DevelopmentBranchAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADE0001000000000000DE01 /* DevelopmentBranchAlerter.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1319,7 +1319,6 @@ C1EE9E802A38D0FB0064784A /* BuildDetails.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = BuildDetails.plist; sourceTree = ""; }; C1EF747128D6A44A00C8C083 /* CrashRecoveryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashRecoveryManager.swift; sourceTree = ""; }; C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExpirationAlerter.swift; sourceTree = ""; }; - FADE0001000000000000DE01 /* DevelopmentBranchAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevelopmentBranchAlerter.swift; sourceTree = ""; }; C1F48FF62995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; C1F7822527CC056900C0919A /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = ""; }; C1F8B1D122375E4200DD66CF /* BolusProgressTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusProgressTableViewCell.swift; sourceTree = ""; }; @@ -1415,6 +1414,7 @@ F5E0BDD827E1D71E0033557E /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Main.strings; sourceTree = ""; }; F5E0BDDA27E1D71F0033557E /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = ""; }; F5E0BDE327E1D7230033557E /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = ""; }; + FADE0001000000000000DE01 /* DevelopmentBranchAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevelopmentBranchAlerter.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 4d4d0eb7ae..217ff2266f 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -374,7 +374,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : " Veiligheidsmeldingen staan UIT" + "value" : " Veiligheids- en kritieke meldingen staan UIT" } }, "pl" : { @@ -1237,7 +1237,7 @@ "nl" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ %2$@ kon je huidige tijdelijke basaalstand niet annuleren, die hoger is dan de nieuwe maximale basaallimiet die je hebt ingesteld. Dit kan leiden tot een hogere insulinetoediening dan gewenst. \n\nOverweeg om de insulinetoediening handmatig te onderbreken en dan direct het hervatten van de basaaltoediening uit te laten voeren met de nieuwe geldende limiet." + "value" : "%@%@ kon je huidige tijdelijke basaalstand niet annuleren, die hoger is dan de nieuwe maximale basaallimiet die je hebt ingesteld. Dit kan leiden tot een hogere insulinetoediening dan gewenst. \n\nOverweeg om de insulinetoediening handmatig te onderbreken en dan direct het hervatten van de basaaltoediening uit te laten voeren met de nieuwe geldende limiet." } }, "pl" : { @@ -35440,7 +35440,27 @@ } }, "Simplify Carb Entry" : { - "comment" : "Subheading for Favorite Foods in Settings" + "comment" : "Subheading for Favorite Foods in Settings", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vereinfache die KH-Eingabe" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koolhydraatinvoer vereenvoudigen" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "简化碳水录入" + } + } + } }, "since %@" : { "comment" : "Format fragment for a start time", From 691afb313b34fa40bc13b2aff26db6cc3f9721c5 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Tue, 6 Oct 2026 22:36:36 -0500 Subject: [PATCH 21/26] Give the dosing tests ten seconds to wait on the data queue (#2558) The one-second waits in LoopDataManagerDosingTests expired on CircleCI's simulator, so two tests asserted against a manager that was still working and the test host then crashed with an index out of range. A longer timeout costs nothing when the tests pass. --- .../Managers/LoopDataManagerDosingTests.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/LoopTests/Managers/LoopDataManagerDosingTests.swift b/LoopTests/Managers/LoopDataManagerDosingTests.swift index a1f26a0e92..ab3acf45fe 100644 --- a/LoopTests/Managers/LoopDataManagerDosingTests.swift +++ b/LoopTests/Managers/LoopDataManagerDosingTests.swift @@ -321,7 +321,7 @@ class LoopDataManagerDosingTests: LoopDataManagerTests { XCTAssertEqual(0, recommendedTempBasal!.unitsPerHour, accuracy: defaultAccuracy) } - func waitOnDataQueue(timeout: TimeInterval = 1.0) { + func waitOnDataQueue(timeout: TimeInterval = 10.0) { let e = expectation(description: "dataQueue") loopDataManager.getLoopState { _, _ in e.fulfill() @@ -345,7 +345,7 @@ class LoopDataManagerDosingTests: LoopDataManagerTests { error = $0 exp.fulfill() } - wait(for: [exp], timeout: 1.0) + wait(for: [exp], timeout: 10.0) XCTAssertNil(error) XCTAssertNil(delegate.recommendation) XCTAssertTrue(dosingDecisionStore.dosingDecisions.isEmpty) @@ -367,7 +367,7 @@ class LoopDataManagerDosingTests: LoopDataManagerTests { error = $0 exp.fulfill() } - wait(for: [exp], timeout: 1.0) + wait(for: [exp], timeout: 10.0) XCTAssertNil(error) XCTAssertEqual(delegate.recommendation, AutomaticDoseRecommendation(basalAdjustment: .cancel)) XCTAssertEqual(dosingDecisionStore.dosingDecisions.count, 1) @@ -386,7 +386,7 @@ class LoopDataManagerDosingTests: LoopDataManagerTests { } XCTAssertFalse(loopDataUpdated) loopDataManager.mutateSettings { $0.maximumBasalRatePerHour = 2.0 } - wait(for: [exp], timeout: 1.0) + wait(for: [exp], timeout: 10.0) XCTAssertTrue(loopDataUpdated) NotificationCenter.default.removeObserver(observer) } @@ -402,7 +402,7 @@ class LoopDataManagerDosingTests: LoopDataManagerTests { exp.fulfill() } automaticDosingStatus.automaticDosingEnabled = false - wait(for: [exp], timeout: 1.0) + wait(for: [exp], timeout: 10.0) let expectedAutomaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: .cancel) XCTAssertEqual(delegate.recommendation, expectedAutomaticDoseRecommendation) XCTAssertEqual(dosingDecisionStore.dosingDecisions.count, 1) @@ -422,7 +422,7 @@ class LoopDataManagerDosingTests: LoopDataManagerTests { exp.fulfill() } loopDataManager.receivedUnreliableCGMReading() - wait(for: [exp], timeout: 1.0) + wait(for: [exp], timeout: 10.0) let expectedAutomaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: .cancel) XCTAssertEqual(delegate.recommendation, expectedAutomaticDoseRecommendation) XCTAssertEqual(dosingDecisionStore.dosingDecisions.count, 1) @@ -441,7 +441,7 @@ class LoopDataManagerDosingTests: LoopDataManagerTests { exp.fulfill() } loopDataManager.loop() - wait(for: [exp], timeout: 1.0) + wait(for: [exp], timeout: 10.0) let expectedAutomaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: TempBasalRecommendation(unitsPerHour: 4.55, duration: .minutes(30))) XCTAssertEqual(delegate.recommendation, expectedAutomaticDoseRecommendation) XCTAssertEqual(dosingDecisionStore.dosingDecisions.count, 1) @@ -465,7 +465,7 @@ class LoopDataManagerDosingTests: LoopDataManagerTests { exp.fulfill() } loopDataManager.loop() - wait(for: [exp], timeout: 1.0) + wait(for: [exp], timeout: 10.0) let expectedAutomaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: TempBasalRecommendation(unitsPerHour: 4.55, duration: .minutes(30))) XCTAssertNil(delegate.recommendation) XCTAssertEqual(dosingDecisionStore.dosingDecisions.count, 1) @@ -496,7 +496,7 @@ class LoopDataManagerDosingTests: LoopDataManagerTests { recommendedBolus = try? loopState.recommendBolus(consideringPotentialCarbEntry: nil, replacingCarbEntry: nil, considerPositiveVelocityAndRC: true) exp.fulfill() } - wait(for: [exp], timeout: 1.0) + wait(for: [exp], timeout: 10.0) XCTAssertEqual(recommendedBolus!.amount, 1.62, accuracy: 0.01) } @@ -508,7 +508,7 @@ class LoopDataManagerDosingTests: LoopDataManagerTests { recommendedBolus = try? loopState.recommendBolus(consideringPotentialCarbEntry: nil, replacingCarbEntry: nil, considerPositiveVelocityAndRC: false) exp.fulfill() } - wait(for: [exp], timeout: 1.0) + wait(for: [exp], timeout: 10.0) XCTAssertEqual(recommendedBolus!.amount, 1.52, accuracy: 0.01) } From d71e70ab321c9ca57386d0cfae211bbcadb7ce52 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sun, 18 Oct 2026 14:02:47 -0500 Subject: [PATCH 22/26] Daily analytics summary, settings state and device models (#2588) Backport of LoopKit/Loop#2574, LoopKit/Loop#2578 and LoopKit/Loop#2582 from next-dev. - Replace the per-cycle "Loop success", "Loop error" and "CGM Fetch" events with one "Daily summary" per local day: loop success count and mean/max duration, loop error counts by issue id, the count of stored CGM readings, the day's UTC offset, and the settings state (closed loop, dosing strategy, integral retrospective correction, glucose based partial application). Running totals are kept in UserDefaults and the summary is sent on the first loop or CGM reading of the next day. - Send that settings state as user properties at launch and whenever it changes, with "Closed loop enabled change", "Dosing strategy change" and "Algorithm experiment change" events. Also send Pump Type and CGM Type at launch, and set them to "None" when the device is removed. - Send Pump Model and CGM Model user properties from the managers' HKDevice.model at setup and launch, and when a status update reports a different model. - Stop sending health data: drop bolus units and carb grams, and send only the kind of override, and its duration, not preset names, targets or sensitivity. - Send "Override Ended" when a preset is deactivated and "Settings Screen" when Settings opens. - Carb and bolus events from the phone are now in session; from the watch and remote commands they stay out of session. - Replace "Alert Issued" with "Alert Displayed": immediate alerts when issued, delayed and repeating alerts after a loop once they have come due without being retracted, and loop-not-running notifications when Loop infers their delivery. dev's glucose samples don't record backfill, so the summary counts CGM readings but has no backfill counts. The summary and state are updated on the main queue, since dev reports loops and readings from other queues. --- Loop/Managers/Alerts/AlertManager.swift | 23 +- Loop/Managers/Alerts/AlertStore.swift | 36 +++ Loop/Managers/AnalyticsServicesManager.swift | 212 +++++++++++++----- Loop/Managers/DeviceDataManager.swift | 20 +- Loop/Managers/LoopAppManager.swift | 18 +- Loop/Managers/ServicesManager.swift | 4 +- Loop/Managers/WatchDataManager.swift | 4 +- .../StatusTableViewController.swift | 1 + Loop/View Models/BolusEntryViewModel.swift | 4 +- .../Managers/Alerts/AlertStoreTests.swift | 27 +++ 10 files changed, 285 insertions(+), 64 deletions(-) diff --git a/Loop/Managers/Alerts/AlertManager.swift b/Loop/Managers/Alerts/AlertManager.swift index 50b99666e2..83f2a12d45 100644 --- a/Loop/Managers/Alerts/AlertManager.swift +++ b/Loop/Managers/Alerts/AlertManager.swift @@ -161,6 +161,24 @@ public final class AlertManager { func loopDidComplete(_ lastLoopDate: Date? = nil) { // use now if there is no lastLoopDate rescheduleLoopNotRunningNotifications(lastLoopDate ?? Date()) + reportDisplayedScheduledAlerts() + } + + private func reportDisplayedScheduledAlerts() { + let now = getCurrentDate() + let start = UserDefaults.standard.scheduledAlertsReportedThrough ?? now + UserDefaults.standard.scheduledAlertsReportedThrough = now + guard start < now else { + return + } + alertStore.lookupScheduledAlertsDue(after: start, through: now) { result in + guard case .success(let alerts) = result else { + return + } + for alert in alerts { + self.analyticsServicesManager.didDisplayAlert(identifier: alert.identifier.value, interruptionLevel: alert.interruptionLevel, scheduled: true) + } + } } private func rescheduleLoopNotRunningNotifications() { @@ -246,6 +264,7 @@ public final class AlertManager { let interruptionLevel: Alert.InterruptionLevel = notification.isCritical ? .critical : .timeSensitive let alert = Alert(identifier: alertIdentifier, foregroundContent: nil, backgroundContent: content, trigger: .immediate, interruptionLevel: interruptionLevel) recordIssued(alert: alert, at: notification.alertAt) + analyticsServicesManager.didDisplayAlert(identifier: alertIdentifier.value, interruptionLevel: interruptionLevel, scheduled: true) } else { stillPendingNotifications.append(notification) } @@ -361,7 +380,9 @@ extension AlertManager: AlertIssuer { deferredAlerts.append(alert) return } - analyticsServicesManager.didIssueAlert(identifier: alert.identifier.value, interruptionLevel: alert.interruptionLevel) + if alert.trigger == .immediate { + analyticsServicesManager.didDisplayAlert(identifier: alert.identifier.value, interruptionLevel: alert.interruptionLevel, scheduled: false) + } scheduleAlertWithSchedulers(alert) alertStore.recordIssued(alert: alert) } diff --git a/Loop/Managers/Alerts/AlertStore.swift b/Loop/Managers/Alerts/AlertStore.swift index d8d6db7e5c..702992adb8 100644 --- a/Loop/Managers/Alerts/AlertStore.swift +++ b/Loop/Managers/Alerts/AlertStore.swift @@ -210,6 +210,42 @@ public class AlertStore { } } + /// Delayed and repeating alerts that came due in (start, end] without being retracted first. + /// Only the latest issue of each identifier counts, since re-issuing replaces the scheduled alert. + public func lookupScheduledAlertsDue(after start: Date, through end: Date, completion: @escaping (Result<[(identifier: Alert.Identifier, interruptionLevel: Alert.InterruptionLevel)], Error>) -> Void) { + managedObjectContext.perform { + do { + let fetchRequest: NSFetchRequest = StoredAlert.fetchRequest() + fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ + NSPredicate(format: "triggerType != %d", Alert.Trigger.immediate.storedType), + NSPredicate(format: "issuedDate > %@ AND issuedDate <= %@", end.addingTimeInterval(-.days(30)) as NSDate, end as NSDate) + ]) + fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "issuedDate", ascending: true) ] + + var latest: [Alert.Identifier: StoredAlert] = [:] + for alert in try self.managedObjectContext.fetch(fetchRequest) { + latest[alert.identifier] = alert + } + + completion(.success(latest.values.compactMap { alert in + guard let interval = alert.triggerInterval?.doubleValue else { + return nil + } + let due = alert.issuedDate.addingTimeInterval(interval) + guard due > start, due <= end else { + return nil + } + if let retractedDate = alert.retractedDate, retractedDate <= due { + return nil + } + return (alert.identifier, alert.interruptionLevel) + })) + } catch { + completion(.failure(error)) + } + } + } + public func lookupAllAcknowledgedUnretractedRepeatingAlerts(completion: @escaping (Result<[StoredAlert], Error>) -> Void) { managedObjectContext.perform { do { diff --git a/Loop/Managers/AnalyticsServicesManager.swift b/Loop/Managers/AnalyticsServicesManager.swift index 861489aba2..122de34575 100644 --- a/Loop/Managers/AnalyticsServicesManager.swift +++ b/Loop/Managers/AnalyticsServicesManager.swift @@ -17,6 +17,10 @@ final class AnalyticsServicesManager { private var analyticsServices = [AnalyticsService]() + var stateProvider: (() -> AnalyticsState)? + + private var lastState: AnalyticsState? + init() {} func addService(_ analyticsService: AnalyticsService) { @@ -70,6 +74,14 @@ final class AnalyticsServicesManager { identify("CGM Type", value: cgmType) } + func identifyPumpModel(_ model: String?) { + identify("Pump Model", value: model ?? "Unknown") + } + + func identifyCGMModel(_ model: String?) { + identify("CGM Model", value: model ?? "Unknown") + } + // MARK: - Screens func didDisplayBolusScreen() { @@ -139,10 +151,6 @@ final class AnalyticsServicesManager { logEvent("Minimum BG Guard change") } - if newValue.dosingEnabled != oldValue.dosingEnabled { - logEvent("Closed loop enabled change") - } - if newValue.basalRateSchedule?.timeZone != oldValue.basalRateSchedule?.timeZone { logEvent("Therapy schedule time zone change") } @@ -160,6 +168,8 @@ final class AnalyticsServicesManager { func pumpWasRemoved() { logEvent("Pump Removed") + identifyPumpType("None") + identifyPumpModel("None") } func pumpWasAdded(identifier: String) { @@ -168,72 +178,116 @@ final class AnalyticsServicesManager { func cgmWasRemoved() { logEvent("CGM Removed") + identifyCGMType("None") + identifyCGMModel("None") } func cgmWasAdded(identifier: String) { logEvent("CGM Added", withProperties: ["identifier" : identifier]) } - func didAddCarbs(source: String, amount: Double, inSession: Bool = false) { - logEvent("Carb entry created", withProperties: ["source" : source, "amount": "\(amount)"], outOfSession: inSession) + func didAddCarbs(source: String) { + logEvent("Carb entry created", withProperties: ["source" : source], outOfSession: source != "Phone") } func didRetryBolus() { logEvent("Bolus Retry") } - func didBolus(source: String, units: Double, inSession: Bool = false) { - logEvent("Bolus set", withProperties: ["source" : source, "units": "\(units)"], outOfSession: true) + func didBolus(source: String) { + logEvent("Bolus set", withProperties: ["source" : source], outOfSession: source != "Phone") } - func didFetchNewCGMData() { - logEvent("CGM Fetch", outOfSession: true) + // MARK: - Daily Summary + + func didStoreGlucose(_ samples: [StoredGlucoseSample]) { + let count = samples.filter { !$0.wasUserEntered }.count + updateDailySummary { $0.glucoseCount += count } } func loopDidSucceed(_ duration: TimeInterval) { - logEvent("Loop success", withProperties: ["duration": duration], outOfSession: true) + updateDailySummary { summary in + summary.loopSuccessCount += 1 + summary.totalLoopDuration += duration + summary.maxLoopDuration = max(summary.maxLoopDuration, duration) + } } func loopDidError(error: LoopError) { - var props = [AnyHashable: Any]() - - props["issueId"] = error.issueId + updateDailySummary { $0.loopErrorCounts[error.issueId, default: 0] += 1 } + } - for (detailKey, detail) in error.issueDetails { - props[detailKey] = detail + func identifyState() { + guard let state = stateProvider?() else { + return + } + identify("Closed Loop", value: state.closedLoop ? "On" : "Off") + identify("Dosing Strategy", value: state.dosingStrategy) + identify("Integral Retrospective Correction", value: state.integralRetrospectiveCorrection ? "On" : "Off") + identify("Glucose Based Partial Application", value: state.glucoseBasedPartialApplication ? "On" : "Off") + lastState = state + } + + // Callers are on the loop and CGM queues; the summary and state are only touched on main + private func updateDailySummary(_ update: @escaping (inout AnalyticsDailySummary) -> Void) { + let now = Date() + DispatchQueue.main.async { + let day = Calendar.current.startOfDay(for: now) + var summary = UserDefaults.standard.analyticsDailySummary ?? AnalyticsDailySummary(day: day) + + if summary.day != day { + self.logEvent("Daily summary", withProperties: summary.properties, outOfSession: true) + summary = AnalyticsDailySummary(day: day) + } + + update(&summary) + + if let state = self.stateProvider?() { + summary.state = state + if let lastState = self.lastState, state != lastState { + self.logStateChanges(from: lastState, to: state) + self.identifyState() + } + } + + UserDefaults.standard.analyticsDailySummary = summary } - - logEvent("Loop error", withProperties: props, outOfSession: true) } - func didIssueAlert(identifier: String, interruptionLevel: Alert.InterruptionLevel) { - logEvent("Alert Issued", withProperties: ["identifier": identifier, "interruptionLevel": interruptionLevel.rawValue]) + private func logStateChanges(from oldState: AnalyticsState, to newState: AnalyticsState) { + if newState.closedLoop != oldState.closedLoop { + logEvent("Closed loop enabled change", withProperties: ["enabled": newState.closedLoop]) + } + if newState.dosingStrategy != oldState.dosingStrategy { + logEvent("Dosing strategy change", withProperties: ["value": newState.dosingStrategy]) + } + if newState.integralRetrospectiveCorrection != oldState.integralRetrospectiveCorrection { + logEvent("Algorithm experiment change", withProperties: ["experiment": "Integral Retrospective Correction", "enabled": newState.integralRetrospectiveCorrection]) + } + if newState.glucoseBasedPartialApplication != oldState.glucoseBasedPartialApplication { + logEvent("Algorithm experiment change", withProperties: ["experiment": "Glucose Based Partial Application", "enabled": newState.glucoseBasedPartialApplication]) + } } - func didEnactOverride(name: String, symbol: String, duration: TemporaryScheduleOverride.Duration, insulinSensitivityMultiplier: Double = 1.0, targetRange: ClosedRange? = nil) - { - let combinedName = "\(symbol) - \(name)" + func didDisplayAlert(identifier: String, interruptionLevel: Alert.InterruptionLevel, scheduled: Bool) { + logEvent("Alert Displayed", withProperties: ["identifier": identifier, "interruptionLevel": interruptionLevel.rawValue, "scheduled": scheduled], outOfSession: scheduled) + } - var properties: [String: Any] = [ - "name": name, - "symbol": symbol, - "sensitivityMultiplier": insulinSensitivityMultiplier, - "nameWithEmoji": combinedName - ] + func didEnactOverride(kind: String, duration: TemporaryScheduleOverride.Duration) { + var properties: [String: Any] = ["kind": kind] - if let targetUpperBound = targetRange?.upperBound.doubleValue(for: HKUnit.milligramsPerDeciliter) { - properties["targetUpperBound"] = targetUpperBound - } - if let targetLowerBound = targetRange?.lowerBound.doubleValue(for: HKUnit.milligramsPerDeciliter) { - properties["targetLowerBound"] = targetLowerBound + switch duration { + case .finite(let interval): + properties["durationMinutes"] = Int(interval.minutes) + case .indefinite: + properties["durationMinutes"] = "indefinite" } - logEvent("Override Enacted", withProperties: properties) } - func didCancelOverride(name: String) { - logEvent("Override Canceled", withProperties: ["name": name]) + func didEndOverride(kind: String) { + logEvent("Override Ended", withProperties: ["kind": kind]) } } @@ -241,24 +295,78 @@ final class AnalyticsServicesManager { // MARK: - PresetActivationObserver extension AnalyticsServicesManager: PresetActivationObserver { func presetActivated(context: TemporaryScheduleOverride.Context, duration: TemporaryScheduleOverride.Duration) { - switch context { - case .legacyWorkout: - didEnactOverride(name: "workout", symbol: "", duration: duration) - case .preMeal: - didEnactOverride(name: "preMeal", symbol: "", duration: duration) - case .custom: - didEnactOverride(name: "custom", symbol: "", duration: duration) - case .preset(let preset): - didEnactOverride(name: preset.name, symbol: preset.symbol, duration: duration, insulinSensitivityMultiplier: preset.settings.effectiveInsulinNeedsScaleFactor, targetRange: preset.settings.targetRange) - } + didEnactOverride(kind: context.analyticsKind, duration: duration) } func presetDeactivated(context: TemporaryScheduleOverride.Context) { - switch context { - case .legacyWorkout: - break - default: - break + didEndOverride(kind: context.analyticsKind) + } +} + +extension TemporaryScheduleOverride.Context { + var analyticsKind: String { + switch self { + case .legacyWorkout: return "workout" + case .preMeal: return "preMeal" + case .custom: return "custom" + case .preset: return "preset" + } + } +} + +struct AnalyticsState: Codable, Equatable { + var closedLoop: Bool + var dosingStrategy: String + var integralRetrospectiveCorrection: Bool + var glucoseBasedPartialApplication: Bool +} + +struct AnalyticsDailySummary: Codable, Equatable { + var day: Date + var utcOffset = TimeZone.current.secondsFromGMT() + var loopSuccessCount = 0 + var totalLoopDuration: TimeInterval = 0 + var maxLoopDuration: TimeInterval = 0 + var loopErrorCounts: [String: Int] = [:] + var glucoseCount = 0 + var state: AnalyticsState? + + var properties: [AnyHashable: Any] { + var properties: [AnyHashable: Any] = [ + "day": ISO8601DateFormatter.string(from: day, timeZone: TimeZone(secondsFromGMT: utcOffset) ?? .current, formatOptions: .withFullDate), + "utcOffset": utcOffset, + "loopSuccessCount": loopSuccessCount, + "meanLoopDuration": loopSuccessCount > 0 ? totalLoopDuration / Double(loopSuccessCount) : 0, + "maxLoopDuration": maxLoopDuration, + "loopErrorCount": loopErrorCounts.values.reduce(0, +), + "loopErrorCounts": loopErrorCounts, + "glucoseCount": glucoseCount + ] + if let state { + properties["closedLoop"] = state.closedLoop + properties["dosingStrategy"] = state.dosingStrategy + properties["integralRetrospectiveCorrection"] = state.integralRetrospectiveCorrection + properties["glucoseBasedPartialApplication"] = state.glucoseBasedPartialApplication + } + return properties + } +} + +extension UserDefaults { + private static let analyticsDailySummaryKey = "com.loopkit.Loop.AnalyticsDailySummary" + private static let scheduledAlertsReportedThroughKey = "com.loopkit.Loop.AnalyticsScheduledAlertsReportedThrough" + + var scheduledAlertsReportedThrough: Date? { + get { object(forKey: Self.scheduledAlertsReportedThroughKey) as? Date } + set { set(newValue, forKey: Self.scheduledAlertsReportedThroughKey) } + } + + var analyticsDailySummary: AnalyticsDailySummary? { + get { + data(forKey: Self.analyticsDailySummaryKey).flatMap { try? JSONDecoder().decode(AnalyticsDailySummary.self, from: $0) } + } + set { + set(newValue.flatMap { try? JSONEncoder().encode($0) }, forKey: Self.analyticsDailySummaryKey) } } } diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 09ee0c11db..8c2b705c0b 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -43,6 +43,8 @@ final class DeviceDataManager { @Published var cgmHasValidSensorSession: Bool + private var cgmDeviceModel: String? + @Published var pumpIsAllowingAutomation: Bool private var lastCGMLoopTrigger: Date = .distantPast @@ -581,6 +583,9 @@ final class DeviceDataManager { switch readingResult { case .newData(let values): loopManager.addGlucoseSamples(values) { result in + if case .success(let samples) = result { + self.analyticsServicesManager.didStoreGlucose(samples) + } if !values.isEmpty { DispatchQueue.main.async { self.cgmStalenessMonitor.cgmGlucoseSamplesAvailable(values) @@ -733,6 +738,8 @@ private extension DeviceDataManager { cgmHasValidSensorSession = cgmManager.cgmManagerStatus.hasValidSensorSession analyticsServicesManager.identifyCGMType(cgmManager.pluginIdentifier) + cgmDeviceModel = cgmManager.cgmManagerStatus.device?.model + analyticsServicesManager.identifyCGMModel(cgmDeviceModel) } if let cgmManagerUI = cgmManager as? CGMManagerUI { @@ -763,6 +770,7 @@ private extension DeviceDataManager { deliveryUncertaintyAlertManager = DeliveryUncertaintyAlertManager(pumpManager: pumpManager, alertPresenter: alertPresenter) analyticsServicesManager.identifyPumpType(pumpManager.pluginIdentifier) + analyticsServicesManager.identifyPumpModel(pumpManager.status.device.model) updatePumpManagerBLEHeartbeatPreference() } @@ -1055,6 +1063,10 @@ extension DeviceDataManager: CGMManagerDelegate { if self.cgmHasValidSensorSession != status.hasValidSensorSession { self.cgmHasValidSensorSession = status.hasValidSensorSession } + if self.cgmDeviceModel != status.device?.model { + self.cgmDeviceModel = status.device?.model + self.analyticsServicesManager.identifyCGMModel(self.cgmDeviceModel) + } } } } @@ -1112,10 +1124,6 @@ extension DeviceDataManager: PumpManagerDelegate { } cgmManager.fetchNewDataIfNeeded { (result) in - if case .newData = result { - self.analyticsServicesManager.didFetchNewCGMData() - } - self.queue.async { self.processCGMReadingResult(cgmManager, readingResult: result) { if self.loopManager.lastLoopCompleted == nil || self.loopManager.lastLoopCompleted!.timeIntervalSinceNow < -.minutes(4.2) { @@ -1157,6 +1165,10 @@ extension DeviceDataManager: PumpManagerDelegate { log.default("PumpManager:%{public}@ did update status: %{public}@", String(describing: type(of: pumpManager)), String(describing: status)) doseStore.device = status.device + + if status.device.model != oldStatus.device.model { + analyticsServicesManager.identifyPumpModel(status.device.model) + } if let newBatteryValue = status.pumpBatteryChargeRemaining, let oldBatteryValue = oldStatus.pumpBatteryChargeRemaining, diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 3971e8c8c8..b9a36d1295 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -260,7 +260,23 @@ class LoopAppManager: NSObject { analyticsServicesManager.identifyWorkspaceGitRevision(workspaceGitRevision) } - analyticsServicesManager.identify("Dosing Strategy", value: settingsManager.loopSettings.automaticDosingStrategy.analyticsValue) + analyticsServicesManager.stateProvider = { [settingsManager = settingsManager!] in + AnalyticsState( + closedLoop: settingsManager.loopSettings.dosingEnabled, + dosingStrategy: settingsManager.loopSettings.automaticDosingStrategy.analyticsValue, + integralRetrospectiveCorrection: UserDefaults.standard.integralRetrospectiveCorrectionEnabled, + glucoseBasedPartialApplication: UserDefaults.standard.glucoseBasedApplicationFactorEnabled + ) + } + analyticsServicesManager.identifyState() + if let pumpManager = deviceDataManager.pumpManager { + analyticsServicesManager.identifyPumpType(pumpManager.pluginIdentifier) + analyticsServicesManager.identifyPumpModel(pumpManager.status.device.model) + } + if let cgmManager = deviceDataManager.cgmManager { + analyticsServicesManager.identifyCGMType(cgmManager.pluginIdentifier) + analyticsServicesManager.identifyCGMModel(cgmManager.cgmManagerStatus.device?.model) + } let serviceNames = deviceDataManager.servicesManager.activeServices.map { $0.pluginIdentifier } analyticsServicesManager.identify("Services", array: serviceNames) diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index 2393ceb073..3fb788f9da 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -322,7 +322,7 @@ extension ServicesManager: ServiceDelegate { try await servicesManagerDelegate?.deliverCarbs(amountInGrams: amountInGrams, absorptionTime: absorptionTime, foodType: foodType, startDate: startDate) await NotificationManager.sendRemoteCarbEntryNotification(amountInGrams: amountInGrams) await remoteDataServicesManager.triggerUpload(for: .carb) - analyticsServicesManager.didAddCarbs(source: "Remote", amount: amountInGrams) + analyticsServicesManager.didAddCarbs(source: "Remote") } catch { await NotificationManager.sendRemoteCarbEntryFailureNotification(for: error, amountInGrams: amountInGrams) throw error @@ -347,7 +347,7 @@ extension ServicesManager: ServiceDelegate { try await servicesManagerDosingDelegate?.deliverBolus(amountInUnits: amountInUnits) await NotificationManager.sendRemoteBolusNotification(amount: amountInUnits) await remoteDataServicesManager.triggerUpload(for: .dose) - analyticsServicesManager.didBolus(source: "Remote", units: amountInUnits) + analyticsServicesManager.didBolus(source: "Remote") } catch { await NotificationManager.sendRemoteBolusFailureNotification(for: error, amountInUnits: amountInUnits) throw error diff --git a/Loop/Managers/WatchDataManager.swift b/Loop/Managers/WatchDataManager.swift index bac60b71dc..b2f8c8db81 100644 --- a/Loop/Managers/WatchDataManager.swift +++ b/Loop/Managers/WatchDataManager.swift @@ -386,7 +386,7 @@ final class WatchDataManager: NSObject { deviceManager.enactBolus(units: bolus.value, activationType: bolus.activationType) { (error) in if error == nil { - self.deviceManager.analyticsServicesManager.didBolus(source: "Watch", units: bolus.value) + self.deviceManager.analyticsServicesManager.didBolus(source: "Watch") } // When we've successfully started the bolus, send a new context with our new prediction @@ -401,7 +401,7 @@ final class WatchDataManager: NSObject { switch result { case .success(let storedCarbEntry): dosingDecision.carbEntry = storedCarbEntry - self.deviceManager.analyticsServicesManager.didAddCarbs(source: "Watch", amount: storedCarbEntry.quantity.doubleValue(for: .gram())) + self.deviceManager.analyticsServicesManager.didAddCarbs(source: "Watch") enactBolus() case .failure(let error): self.log.error("%{public}@", String(describing: error)) diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 3e124fa7f5..2e81f1ad52 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -1581,6 +1581,7 @@ final class StatusTableViewController: LoopChartsTableViewController { } private func presentSettings() { + deviceManager.analyticsServicesManager.didDisplaySettingsScreen() let deletePumpDataFunc: () -> PumpManagerViewModel.DeleteTestingDataFunc? = { [weak self] in (self?.deviceManager.pumpManager is TestingPumpManager) ? { [weak self] in self?.deviceManager.deleteTestingPumpData() diff --git a/Loop/View Models/BolusEntryViewModel.swift b/Loop/View Models/BolusEntryViewModel.swift index a86f20e0cc..20735e7fd7 100644 --- a/Loop/View Models/BolusEntryViewModel.swift +++ b/Loop/View Models/BolusEntryViewModel.swift @@ -407,7 +407,7 @@ final class BolusEntryViewModel: ObservableObject { } if let storedCarbEntry = await saveCarbEntry(carbEntry, replacingEntry: originalCarbEntry) { self.dosingDecision.carbEntry = storedCarbEntry - self.analyticsServicesManager?.didAddCarbs(source: "Phone", amount: storedCarbEntry.quantity.doubleValue(for: .gram())) + self.analyticsServicesManager?.didAddCarbs(source: "Phone") } else { self.presentAlert(.carbEntryPersistenceFailure) return false @@ -422,7 +422,7 @@ final class BolusEntryViewModel: ObservableObject { if amountToDeliver > 0 { savedPreMealOverride = nil delegate.enactBolus(units: amountToDeliver, activationType: activationType, completion: { _ in - self.analyticsServicesManager?.didBolus(source: "Phone", units: amountToDeliver) + self.analyticsServicesManager?.didBolus(source: "Phone") }) } return true diff --git a/LoopTests/Managers/Alerts/AlertStoreTests.swift b/LoopTests/Managers/Alerts/AlertStoreTests.swift index bb9d109633..e7419b6693 100644 --- a/LoopTests/Managers/Alerts/AlertStoreTests.swift +++ b/LoopTests/Managers/Alerts/AlertStoreTests.swift @@ -91,6 +91,33 @@ class AlertStoreTests: XCTestCase { XCTAssertEqual(999, newAnchor?.modificationCounter) } + func testLookupScheduledAlertsDue() { + let issued = Self.historicDate + let due = issued.addingTimeInterval(Self.delayedAlertDelay) + let expect = self.expectation(description: #function) + alertStore.recordIssued(alert: alert1, at: issued, completion: self.expectSuccess { + self.alertStore.recordIssued(alert: self.delayedAlert, at: issued, completion: self.expectSuccess { + self.alertStore.recordIssued(alert: self.repeatingAlert, at: issued, completion: self.expectSuccess { + self.alertStore.recordRetraction(of: Self.repeatingAlertIdentifier, at: due.addingTimeInterval(-1), completion: self.expectSuccess { + self.alertStore.lookupScheduledAlertsDue(after: issued, through: due.addingTimeInterval(1), completion: self.expectSuccess { dueAlerts in + XCTAssertEqual([Self.delayedAlertIdentifier], dueAlerts.map { $0.identifier }) + self.alertStore.lookupScheduledAlertsDue(after: issued, through: due.addingTimeInterval(-1), completion: self.expectSuccess { dueAlerts in + XCTAssertTrue(dueAlerts.isEmpty) + self.alertStore.recordIssued(alert: self.delayedAlert, at: issued.addingTimeInterval(10), completion: self.expectSuccess { + self.alertStore.lookupScheduledAlertsDue(after: issued, through: due.addingTimeInterval(1), completion: self.expectSuccess { dueAlerts in + XCTAssertTrue(dueAlerts.isEmpty) + expect.fulfill() + }) + }) + }) + }) + }) + }) + }) + }) + wait(for: [expect], timeout: Self.defaultTimeout) + } + func testRecordIssued() { let expect = self.expectation(description: #function) alertStore.recordIssued(alert: alert1, at: Self.historicDate, completion: self.expectSuccess { From e959503b18a3138890cd4a91d66a5af654eb543f Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sun, 18 Oct 2026 14:03:07 -0500 Subject: [PATCH 23/26] Submit issue reports to the LoopKit developers; require iOS 17.6 (#2586) * Require iOS 17.6 The project-level 15.1 setting overrode Loop.xcconfig, so the app still targeted iOS 15.1. Remove it and raise Loop.xcconfig to 17.6, which matches next-dev and the plugins that already require it. * Submit issue reports to the LoopKit developers Backport of LoopKit/Loop#2585 from next-dev. Settings > Support > Report Issue now opens one screen where the person either submits a report to ingest.loopkit.org/v1/issues or exports Loop's diagnostic report locally, which opens the existing issue report screen. A submitted report has a description, optional title and screenshots. It is anonymous unless the person fills in a name, email, Zulip or GitHub contact, and the analytics device ID is never sent. Build and device details (version, build, branch, revisions, device model, OS, pump and CGM model) are listed on the screen and sent with every report. Loop's diagnostic report is attached as text by default; the toggle says it contains health data. Photos other than PNG are converted to JPEG. dev has no build customizations list, so that context key is left out. The diagnostic report and the pump and CGM models come from the settings delegate, since dev's settings view model has no device manager. * Include build customizations in issue reports Backport of LoopKit/Loop#2577 from next-dev. capture-build-details.sh records each *.loopcustomization file that the LoopWorkspace customization script adds, the diagnostic report lists them, and an issue report sends them as "customizations" with the other build details. --- Common/Models/BuildDetails.swift | 4 + Loop.xcconfig | 2 +- Loop.xcodeproj/project.pbxproj | 10 +- Loop/Managers/DeviceDataManager.swift | 1 + Loop/Managers/IssueReportSubmitter.swift | 139 ++++++++++ .../StatusTableViewController.swift | 14 + Loop/View Models/SettingsViewModel.swift | 28 ++ Loop/Views/SendIssueReportView.swift | 254 ++++++++++++++++++ Loop/Views/SettingsView.swift | 10 +- Scripts/capture-build-details.sh | 14 + 10 files changed, 469 insertions(+), 7 deletions(-) create mode 100644 Loop/Managers/IssueReportSubmitter.swift create mode 100644 Loop/Views/SendIssueReportView.swift diff --git a/Common/Models/BuildDetails.swift b/Common/Models/BuildDetails.swift index b71e265885..aed7cf9c4b 100644 --- a/Common/Models/BuildDetails.swift +++ b/Common/Models/BuildDetails.swift @@ -66,6 +66,10 @@ class BuildDetails { return dict["com-loopkit-LoopWorkspace-git-branch"] as? String } + var customizations: [String] { + return dict["com-loopkit-Loop-customizations"] as? [String] ?? [] + } + /// Returns a dictionary of submodule details. /// The keys are the submodule names, and the values are tuples (branch, commitSHA). var submodules: [String: (branch: String, commitSHA: String)] { diff --git a/Loop.xcconfig b/Loop.xcconfig index 7827f574d5..c40d01accc 100644 --- a/Loop.xcconfig +++ b/Loop.xcconfig @@ -43,7 +43,7 @@ LOOP_PROVISIONING_PROFILE_SPECIFIER_WATCHAPP_RELEASE = LOOP_PROVISIONING_PROFILE_SPECIFIER_WATCHAPP_EXTENSION_RELEASE = // Min iOS Version [DEFAULT] -IPHONEOS_DEPLOYMENT_TARGET = 16.2 +IPHONEOS_DEPLOYMENT_TARGET = 17.6 // Base string for opening app via URL [DEFAULT] URL_SCHEME_NAME = $(MAIN_APP_DISPLAY_NAME) diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 8f8f9dce09..504223dd3a 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -54,6 +54,7 @@ 1D4A3E2D2478628500FD601B /* StoredAlert+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4A3E2B2478628500FD601B /* StoredAlert+CoreDataClass.swift */; }; 1D4A3E2E2478628500FD601B /* StoredAlert+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4A3E2C2478628500FD601B /* StoredAlert+CoreDataProperties.swift */; }; 1D63DEA526E950D400F46FA5 /* SupportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D63DEA426E950D400F46FA5 /* SupportManager.swift */; }; + C1A55E012E9F000000A10002 /* IssueReportSubmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A55E012E9F000000A10001 /* IssueReportSubmitter.swift */; }; 1D6B1B6726866D89009AC446 /* AlertPermissionsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D6B1B6626866D89009AC446 /* AlertPermissionsChecker.swift */; }; 1D70C40126EC0F9D00C62570 /* SupportManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D70C40026EC0F9D00C62570 /* SupportManagerTests.swift */; }; 1D80313D24746274002810DF /* AlertStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D80313C24746274002810DF /* AlertStoreTests.swift */; }; @@ -344,6 +345,7 @@ A987CD4924A58A0100439ADC /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = A987CD4824A58A0100439ADC /* ZipArchive.swift */; }; A999D40624663D18004C89D4 /* PumpManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A999D40524663D18004C89D4 /* PumpManagerError.swift */; }; A9A056B324B93C62007CF06D /* CriticalEventLogExportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A056B224B93C62007CF06D /* CriticalEventLogExportView.swift */; }; + C1A55E012E9F000000A10004 /* SendIssueReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A55E012E9F000000A10003 /* SendIssueReportView.swift */; }; A9A056B524B94123007CF06D /* CriticalEventLogExportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A056B424B94123007CF06D /* CriticalEventLogExportViewModel.swift */; }; A9A63F8E246B271600588D5B /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897341CD2F7DE00223065 /* NSTimeInterval.swift */; }; A9B607B0247F000F00792BE4 /* UserNotifications+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B607AF247F000F00792BE4 /* UserNotifications+Loop.swift */; }; @@ -789,6 +791,7 @@ 1D4A3E2B2478628500FD601B /* StoredAlert+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoredAlert+CoreDataClass.swift"; sourceTree = ""; }; 1D4A3E2C2478628500FD601B /* StoredAlert+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoredAlert+CoreDataProperties.swift"; sourceTree = ""; }; 1D63DEA426E950D400F46FA5 /* SupportManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportManager.swift; sourceTree = ""; }; + C1A55E012E9F000000A10001 /* IssueReportSubmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueReportSubmitter.swift; sourceTree = ""; }; 1D6B1B6626866D89009AC446 /* AlertPermissionsChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPermissionsChecker.swift; sourceTree = ""; }; 1D70C40026EC0F9D00C62570 /* SupportManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportManagerTests.swift; sourceTree = ""; }; 1D80313C24746274002810DF /* AlertStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertStoreTests.swift; sourceTree = ""; }; @@ -1154,6 +1157,7 @@ A987CD4824A58A0100439ADC /* ZipArchive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipArchive.swift; sourceTree = ""; }; A999D40524663D18004C89D4 /* PumpManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpManagerError.swift; sourceTree = ""; }; A9A056B224B93C62007CF06D /* CriticalEventLogExportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalEventLogExportView.swift; sourceTree = ""; }; + C1A55E012E9F000000A10003 /* SendIssueReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendIssueReportView.swift; sourceTree = ""; }; A9A056B424B94123007CF06D /* CriticalEventLogExportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalEventLogExportViewModel.swift; sourceTree = ""; }; A9B607AF247F000F00792BE4 /* UserNotifications+Loop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserNotifications+Loop.swift"; sourceTree = ""; }; A9B996EF27235191002DC09C /* LoopWarning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopWarning.swift; sourceTree = ""; }; @@ -1976,6 +1980,7 @@ 149A28BC2A853E6C00052EDF /* CarbEntryView.swift */, 431A8C3F1EC6E8AB00823B9C /* CircleMaskView.swift */, A9A056B224B93C62007CF06D /* CriticalEventLogExportView.swift */, + C1A55E012E9F000000A10003 /* SendIssueReportView.swift */, C191D2A025B3ACAA00C26C0B /* DosingStrategySelectionView.swift */, 149A28E32A8A63A700052EDF /* FavoriteFoodDetailView.swift */, 142CB75A2A60BFC30075748A /* FavoriteFoodsView.swift */, @@ -2033,6 +2038,7 @@ B470F5832AB22B5100049695 /* StatefulPluggable.swift */, 43FCEEA8221A615B0013DD30 /* StatusChartsManager.swift */, 1D63DEA426E950D400F46FA5 /* SupportManager.swift */, + C1A55E012E9F000000A10001 /* IssueReportSubmitter.swift */, 4F70C20F1DE8FAC5006380B7 /* ExtensionDataManager.swift */, 89ADE13A226BFA0F0067222B /* TestingScenariosManager.swift */, 1D82E69F25377C6B009131FB /* TrustedTimeChecker.swift */, @@ -3389,6 +3395,7 @@ 897A5A9624C2175B00C4E71D /* BolusEntryView.swift in Sources */, 4F70C2131DE90339006380B7 /* StatusExtensionContext.swift in Sources */, A9A056B324B93C62007CF06D /* CriticalEventLogExportView.swift in Sources */, + C1A55E012E9F000000A10004 /* SendIssueReportView.swift in Sources */, 43C05CC521EC29E3006FB252 /* TextFieldTableViewCell.swift in Sources */, 4FF4D1001E18374700846527 /* WatchContext.swift in Sources */, C1D289B522F90A52003FFBD9 /* BasalDeliveryState.swift in Sources */, @@ -3509,6 +3516,7 @@ A96DAC2C2838F31200D94E38 /* SharedLogging.swift in Sources */, 4302F4E31D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift in Sources */, 1D63DEA526E950D400F46FA5 /* SupportManager.swift in Sources */, + C1A55E012E9F000000A10002 /* IssueReportSubmitter.swift in Sources */, 4FC8C8011DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift in Sources */, 43E93FB61E469A4000EAB8DB /* NumberFormatter.swift in Sources */, C1FB428C217806A400FAB378 /* StateColorPalette.swift in Sources */, @@ -4283,7 +4291,6 @@ GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.1; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZED_STRING_MACRO_NAMES = ( NSLocalizedString, @@ -4395,7 +4402,6 @@ GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.1; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZED_STRING_MACRO_NAMES = ( NSLocalizedString, diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 8c2b705c0b..cabca1e6e0 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -1753,6 +1753,7 @@ extension DeviceDataManager: DeviceSupportDelegate { "* xcodeVersion: \(BuildDetails.default.xcodeVersion ?? "N/A")", "* Workspace branch: \(BuildDetails.default.workspaceGitBranch ?? "N/A")", "* Workspace SHA: \(BuildDetails.default.workspaceGitRevision ?? "N/A")", + "* Customizations: \(BuildDetails.default.customizations.isEmpty ? "None" : BuildDetails.default.customizations.joined(separator: ", "))", "* Submodule name: branch, SHA", "\(submodulesInfo)", "", diff --git a/Loop/Managers/IssueReportSubmitter.swift b/Loop/Managers/IssueReportSubmitter.swift new file mode 100644 index 0000000000..68669dd2f3 --- /dev/null +++ b/Loop/Managers/IssueReportSubmitter.swift @@ -0,0 +1,139 @@ +// +// IssueReportSubmitter.swift +// Loop +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import Foundation + +struct IssueReport { + struct Contact: Encodable { + var name: String? + var email: String? + var zulip: String? + var github: String? + } + + struct Attachment { + var filename: String + var contentType: String + var data: Data + } + + var title: String? + var description: String + var contact: Contact? + var context: [String: IssueReportContextValue] + var attachments: [Attachment] +} + +enum IssueReportContextValue: Encodable, Equatable { + case string(String) + case strings([String]) + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .string(let value): try container.encode(value) + case .strings(let value): try container.encode(value) + } + } +} + +enum IssueReportSubmitterError: LocalizedError { + case server(status: Int, reason: String?) + case invalidResponse + case network(Error) + + var isRetryable: Bool { + switch self { + case .server(let status, _): return status >= 500 + case .invalidResponse, .network: return true + } + } + + var errorDescription: String? { + switch self { + case .server(413, _): + return NSLocalizedString("The report is too large. Try fewer or smaller screenshots.", comment: "Issue report error when the upload is too large") + case .server(415, _): + return NSLocalizedString("One of the attachments isn't a supported image type.", comment: "Issue report error when an attachment type is refused") + case .server(429, _): + return NSLocalizedString("Too many reports have been sent from this network recently. Please try again later.", comment: "Issue report error when rate limited") + case .server(let status, let reason): + return String(format: NSLocalizedString("The server couldn't accept the report (%1$d %2$@).", comment: "Issue report error with HTTP status (1) and reason (2)"), status, reason ?? "") + case .invalidResponse: + return NSLocalizedString("The server sent an unexpected response.", comment: "Issue report error for an unreadable response") + case .network(let error): + return error.localizedDescription + } + } +} + +struct IssueReportSubmitter { + static let endpoint = URL(string: "https://ingest.loopkit.org/v1/issues")! + static let apiKey = "7dd7414785560c0dd1ef802ac10c00b4" + + var session: URLSession = .shared + + func submit(_ report: IssueReport) async throws -> String { + let boundary = "Boundary-\(UUID().uuidString)" + var request = URLRequest(url: Self.endpoint) + request.httpMethod = "POST" + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + + let body = try Self.body(for: report, boundary: boundary) + let data: Data + let response: URLResponse + do { + (data, response) = try await session.upload(for: request, from: body) + } catch { + throw IssueReportSubmitterError.network(error) + } + guard let response = response as? HTTPURLResponse else { + throw IssueReportSubmitterError.invalidResponse + } + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] + guard response.statusCode == 200 else { + throw IssueReportSubmitterError.server(status: response.statusCode, reason: json?["error"] as? String) + } + guard let id = json?["id"] as? String else { + throw IssueReportSubmitterError.invalidResponse + } + return id + } + + static func body(for report: IssueReport, boundary: String) throws -> Data { + struct Payload: Encodable { + var title: String? + var description: String + var contact: IssueReport.Contact? + var context: [String: IssueReportContextValue] + } + let payload = try JSONEncoder().encode(Payload(title: report.title, description: report.description, contact: report.contact, context: report.context)) + + var body = Data() + func appendPart(name: String, filename: String? = nil, contentType: String? = nil, data: Data) { + var header = "--\(boundary)\r\nContent-Disposition: form-data; name=\"\(name)\"" + if let filename { + header += "; filename=\"\(filename.replacingOccurrences(of: "\"", with: ""))\"" + } + header += "\r\n" + if let contentType { + header += "Content-Type: \(contentType)\r\n" + } + body.append(Data((header + "\r\n").utf8)) + body.append(data) + body.append(Data("\r\n".utf8)) + } + + appendPart(name: "client", data: Data(apiKey.utf8)) + appendPart(name: "report", contentType: "application/json", data: payload) + for attachment in report.attachments { + appendPart(name: "attachment", filename: attachment.filename, contentType: attachment.contentType, data: attachment.data) + } + body.append(Data("--\(boundary)--\r\n".utf8)) + return body + } +} diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 2e81f1ad52..eb91cc21d1 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -2234,6 +2234,20 @@ extension StatusTableViewController: SettingsViewModelDelegate { self.show(vc, sender: nil) } } + + func generateDiagnosticReport() async -> String { + await withCheckedContinuation { continuation in + deviceManager.generateDiagnosticReport { continuation.resume(returning: $0) } + } + } + + var pumpModel: String? { + deviceManager.pumpManager.map { $0.status.device.model ?? "Unknown" } + } + + var cgmModel: String? { + deviceManager.cgmManager.map { $0.cgmManagerStatus.device?.model ?? "Unknown" } + } } // MARK: - Services delegation diff --git a/Loop/View Models/SettingsViewModel.swift b/Loop/View Models/SettingsViewModel.swift index d4b48766b3..672dfa449e 100644 --- a/Loop/View Models/SettingsViewModel.swift +++ b/Loop/View Models/SettingsViewModel.swift @@ -53,7 +53,10 @@ public protocol SettingsViewModelDelegate: AnyObject { func dosingEnabledChanged(_: Bool) func dosingStrategyChanged(_: AutomaticDosingStrategy) func didTapIssueReport() + func generateDiagnosticReport() async -> String var closedLoopDescriptiveText: String? { get } + var pumpModel: String? { get } + var cgmModel: String? { get } } public class SettingsViewModel: ObservableObject { @@ -69,6 +72,31 @@ public class SettingsViewModel: ObservableObject { func didTapIssueReport() { delegate?.didTapIssueReport() } + + var generateDiagnosticReport: (() async -> String)? { + guard let delegate else { + return nil + } + return { [weak delegate] in await delegate?.generateDiagnosticReport() ?? "" } + } + + @MainActor + var issueReportContext: [String: IssueReportContextValue] { + let buildDetails = BuildDetails.default + var context: [String: IssueReportContextValue] = [ + "appVersion": .string(Bundle.main.shortVersionString), + "buildNumber": .string(Bundle.main.version), + "customizations": .strings(buildDetails.customizations), + "deviceModel": .string(UIDevice.current.modelIdentifier), + "osVersion": .string(UIDevice.current.systemVersion), + ] + context["branch"] = (buildDetails.workspaceGitBranch ?? buildDetails.gitBranch).map { .string($0) } + context["workspaceRevision"] = buildDetails.workspaceGitRevision.map { .string($0) } + context["loopRevision"] = buildDetails.gitRevision.map { .string($0) } + context["pumpModel"] = delegate?.pumpModel.map { .string($0) } + context["cgmModel"] = delegate?.cgmModel.map { .string($0) } + return context + } var availableSupports: [SupportUI] let pumpManagerSettingsViewModel: PumpManagerViewModel diff --git a/Loop/Views/SendIssueReportView.swift b/Loop/Views/SendIssueReportView.swift new file mode 100644 index 0000000000..5eb2ca73ee --- /dev/null +++ b/Loop/Views/SendIssueReportView.swift @@ -0,0 +1,254 @@ +// +// SendIssueReportView.swift +// Loop +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import PhotosUI +import SwiftUI + +struct SendIssueReportView: View { + let context: [String: IssueReportContextValue] + let generateDiagnosticReport: (() async -> String)? + let exportLocally: () -> Void + + @Environment(\.dismiss) private var dismiss + + @State private var title = "" + @State private var description = "" + @State private var isAnonymous = true + @State private var contact = IssueReport.Contact() + @State private var includeDiagnosticReport = true + @State private var photoSelection: [PhotosPickerItem] = [] + @State private var screenshots: [Screenshot] = [] + @State private var isSending = false + @State private var sentReportID: String? + @State private var sendError: IssueReportSubmitterError? + @State private var showingError = false + + private static let maxDescriptionLength = 20_000 + private static let maxScreenshots = 8 + private static let maxAttachmentBytes = 10_000_000 + + private struct Screenshot: Identifiable { + let id = UUID() + let image: UIImage + let attachment: IssueReport.Attachment + } + + var body: some View { + Form { + Section(footer: Text(String(format: NSLocalizedString("%1$d of %2$d characters", comment: "Issue report description length (1: used)(2: limit)"), description.count, Self.maxDescriptionLength))) { + TextField(NSLocalizedString("Title (optional)", comment: "Placeholder for the issue report title"), text: $title) + ZStack(alignment: .topLeading) { + if description.isEmpty { + Text(NSLocalizedString("Describe what you saw and what you expected to happen", comment: "Placeholder for the issue report description")) + .foregroundColor(Color(.placeholderText)) + .padding(.top, 8) + .padding(.leading, 4) + } + TextEditor(text: $description) + .frame(minHeight: 160) + } + } + + screenshotsSection + + Section(footer: Text(isAnonymous ? + NSLocalizedString("The report won't include your name, contact details or anything else that identifies you.", comment: "Footer for an anonymous issue report") : + NSLocalizedString("Fill in any of these so developers can follow up with you.", comment: "Footer for issue report contact details")) + ) { + Toggle(NSLocalizedString("Submit Anonymously", comment: "Toggle for submitting an issue report without contact details"), isOn: $isAnonymous) + if !isAnonymous { + TextField(NSLocalizedString("Name", comment: "Issue report contact name placeholder"), text: binding(\.name)) + .textContentType(.name) + TextField(NSLocalizedString("Email", comment: "Issue report contact email placeholder"), text: binding(\.email)) + .textContentType(.emailAddress) + .keyboardType(.emailAddress) + .textInputAutocapitalization(.never) + TextField(NSLocalizedString("Zulip Name", comment: "Issue report contact Zulip placeholder"), text: binding(\.zulip)) + .textInputAutocapitalization(.never) + TextField(NSLocalizedString("GitHub Username", comment: "Issue report contact GitHub placeholder"), text: binding(\.github)) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + } + } + + if generateDiagnosticReport != nil { + Section(footer: Text(NSLocalizedString("Attaches Loop's issue report as a text file. It contains health data such as recent glucose, insulin, carbs and your therapy settings.", comment: "Footer for the include diagnostic report toggle"))) { + Toggle(NSLocalizedString("Include Diagnostic Report", comment: "Toggle for attaching the diagnostic report to an issue report"), isOn: $includeDiagnosticReport) + } + } + + Section(header: Text(NSLocalizedString("Also Sent", comment: "Header for the build and device details sent with an issue report")), + footer: Text(NSLocalizedString("Reports go to the LoopKit development team and are visible only to them. Your IP address is used only to limit how often reports can be sent and is never stored.", comment: "Privacy footer for issue reports"))) + { + ForEach(context.keys.sorted(), id: \.self) { key in + HStack { + Text(key) + Spacer() + Text(Self.displayValue(context[key])) + .foregroundColor(.secondary) + .multilineTextAlignment(.trailing) + } + .font(.footnote) + } + } + + Section(footer: Text(NSLocalizedString("Opens Loop's diagnostic report so you can save it or share it yourself. Nothing is sent to the developers.", comment: "Footer for exporting the issue report locally"))) { + Button(NSLocalizedString("Export Locally Instead", comment: "Button to open the diagnostic report for local export instead of submitting"), action: exportLocally) + } + } + .navigationTitle(NSLocalizedString("Report Issue", comment: "Title of the issue report screen")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + if isSending { + ProgressView() + } else { + Button(NSLocalizedString("Submit", comment: "Button to submit an issue report to the developers")) { + send() + } + .disabled(!canSend) + } + } + } + .disabled(isSending) + .onChange(of: photoSelection) { _, items in + loadScreenshots(items) + } + .alert(NSLocalizedString("Report Submitted", comment: "Title of the alert after an issue report was submitted"), isPresented: Binding(get: { sentReportID != nil }, set: { if !$0 { sentReportID = nil } })) { + Button(NSLocalizedString("Copy ID", comment: "Button to copy the issue report ID")) { + UIPasteboard.general.string = sentReportID + dismiss() + } + Button(NSLocalizedString("Done", comment: "Button to close the issue report sent alert"), role: .cancel) { + dismiss() + } + } message: { + Text(String(format: NSLocalizedString("Thank you. Your report ID is %1$@. Mention it if you discuss the problem on Zulip or GitHub.", comment: "Message after an issue report was sent (1: report ID)"), sentReportID ?? "")) + } + .alert(NSLocalizedString("Unable to Submit Report", comment: "Title of the alert when an issue report fails"), isPresented: $showingError, presenting: sendError) { error in + if error.isRetryable { + Button(NSLocalizedString("Try Again", comment: "Button to retry sending an issue report")) { + send() + } + } + Button(NSLocalizedString("OK", comment: "Button to close the issue report error alert"), role: .cancel) {} + } message: { error in + Text(error.localizedDescription) + } + } + + private var screenshotsSection: some View { + Section(footer: Text(String(format: NSLocalizedString("Up to %1$d screenshots or photos.", comment: "Footer for issue report screenshots (1: limit)"), Self.maxScreenshots))) { + if !screenshots.isEmpty { + ScrollView(.horizontal) { + HStack { + ForEach(screenshots) { screenshot in + Image(uiImage: screenshot.image) + .resizable() + .scaledToFit() + .frame(height: 120) + .clipShape(RoundedRectangle(cornerRadius: 6)) + } + } + } + } + PhotosPicker(selection: $photoSelection, maxSelectionCount: Self.maxScreenshots, matching: .images) { + Text(screenshots.isEmpty ? + NSLocalizedString("Add Screenshots", comment: "Button to choose screenshots for an issue report") : + NSLocalizedString("Change Screenshots", comment: "Button to change the screenshots on an issue report")) + } + } + } + + private var trimmedDescription: String { + description.trimmingCharacters(in: .whitespacesAndNewlines) + } + + private var canSend: Bool { + !trimmedDescription.isEmpty && description.count <= Self.maxDescriptionLength + } + + private func binding(_ keyPath: WritableKeyPath) -> Binding { + Binding(get: { contact[keyPath: keyPath] ?? "" }, set: { contact[keyPath: keyPath] = $0 }) + } + + private func loadScreenshots(_ items: [PhotosPickerItem]) { + Task { + var loaded: [Screenshot] = [] + for (index, item) in items.enumerated() { + guard let data = try? await item.loadTransferable(type: Data.self), let screenshot = Self.screenshot(from: data, index: index + 1) else { + continue + } + loaded.append(screenshot) + } + screenshots = loaded + } + } + + // The server accepts only PNG and JPEG, so anything else (HEIC from the camera) is converted + private static func screenshot(from data: Data, index: Int) -> Screenshot? { + guard let image = UIImage(data: data) else { + return nil + } + if data.starts(with: [0x89, 0x50, 0x4E, 0x47]), data.count <= maxAttachmentBytes { + return Screenshot(image: image, attachment: .init(filename: "screenshot-\(index).png", contentType: "image/png", data: data)) + } + for quality in [0.8, 0.5, 0.3] { + if let jpeg = image.jpegData(compressionQuality: quality), jpeg.count <= maxAttachmentBytes { + return Screenshot(image: image, attachment: .init(filename: "screenshot-\(index).jpg", contentType: "image/jpeg", data: jpeg)) + } + } + return nil + } + + private static func displayValue(_ value: IssueReportContextValue?) -> String { + switch value { + case .string(let string): return string + case .strings(let strings): return strings.isEmpty ? "–" : strings.joined(separator: ", ") + case nil: return "" + } + } + + private func send() { + isSending = true + Task { + var attachments = screenshots.map(\.attachment) + if includeDiagnosticReport, let generateDiagnosticReport { + let report = await generateDiagnosticReport() + // Stay under the server's per-file limit; decoding repairs a character cut in half + let text = String(decoding: report.utf8.prefix(Self.maxAttachmentBytes - 4), as: UTF8.self) + attachments.append(.init(filename: "diagnostic-report.txt", contentType: "text/plain; charset=utf-8", data: Data(text.utf8))) + } + let report = IssueReport( + title: title.isEmpty ? nil : String(title.prefix(200)), + description: trimmedDescription, + contact: isAnonymous ? nil : Self.trimmed(contact), + context: context, + attachments: attachments + ) + do { + sentReportID = try await IssueReportSubmitter().submit(report) + } catch let error as IssueReportSubmitterError { + sendError = error + showingError = true + } catch { + sendError = .network(error) + showingError = true + } + isSending = false + } + } + + private static func trimmed(_ contact: IssueReport.Contact) -> IssueReport.Contact? { + func clean(_ value: String?) -> String? { + let value = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + return value.isEmpty ? nil : value + } + let result = IssueReport.Contact(name: clean(contact.name), email: clean(contact.email), zulip: clean(contact.zulip), github: clean(contact.github)) + return [result.name, result.email, result.zulip, result.github].allSatisfy({ $0 == nil }) ? nil : result + } +} diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 5575fa8c33..b0f2567f18 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -457,10 +457,12 @@ extension SettingsView { private var supportSection: some View { Section(header: SectionHeader(label: NSLocalizedString("Support", comment: "The title of the support section in settings"))) { - Button(action: { - self.viewModel.didTapIssueReport() - }) { - Text("Issue Report", comment: "The title text for the issue report menu item") + NavigationLink(destination: SendIssueReportView( + context: viewModel.issueReportContext, + generateDiagnosticReport: viewModel.generateDiagnosticReport, + exportLocally: viewModel.didTapIssueReport + )) { + Text(NSLocalizedString("Report Issue", comment: "The title text for the issue report menu item")) } ForEach(pluginMenuItems.filter( { $0.section == .support })) { diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index b78eb1c592..3d855c8d50 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -101,6 +101,20 @@ then popd . > /dev/null fi +# --- Customizations --- +# Each customization adds an empty .loopcustomization file at the workspace root or a submodule root. +customizations_key="com-loopkit-Loop-customizations" +/usr/libexec/PlistBuddy -c "Delete :${customizations_key}" "${info_plist_path}" 2>/dev/null || true +/usr/libexec/PlistBuddy -c "Add :${customizations_key} array" "${info_plist_path}" +if [ -e ../.git ]; then + customizations_root=.. +else + customizations_root=. +fi +find "${customizations_root}" -maxdepth 2 -name '*.loopcustomization' -exec basename {} .loopcustomization \; | sort -u | while read -r identifier; do + /usr/libexec/PlistBuddy -c "Add :${customizations_key}: string ${identifier}" "${info_plist_path}" +done + # --- Root repo details --- # Retrieve current branch (or tag) and commit SHA. git_branch=$(git symbolic-ref --short -q HEAD || echo "") From 05a7f1bf31432555ef2f378458872be6e37f2b28 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Tue, 20 Oct 2026 12:24:53 -0500 Subject: [PATCH 24/26] Make the muted loop-not-running test wait for the reschedule (#2592) testRescheduleMutedLoopNotLoopingAlerts failed now and then on the first run after a simulator had been idle. Changing the muter reschedules the loop-not-running notifications from ExtensionDataManager.lastLoopCompleted, which the host app refreshes only after launch; a value more than 20 minutes old schedules nothing, leaving the unmuted notifications from loopDidComplete in place. Earlier runs also leave a 4-hour mute in UserDefaults, which hides the problem until it expires. The test now sets the last loop date it reschedules from, and reads the pending notifications until the muted versions replace the others, for up to 5 seconds, instead of reading them once. --- .../Managers/Alerts/AlertManagerTests.swift | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/LoopTests/Managers/Alerts/AlertManagerTests.swift b/LoopTests/Managers/Alerts/AlertManagerTests.swift index 5eeca9cebd..51fd42ea0a 100644 --- a/LoopTests/Managers/Alerts/AlertManagerTests.swift +++ b/LoopTests/Managers/Alerts/AlertManagerTests.swift @@ -490,21 +490,23 @@ class AlertManagerTests: XCTestCase { UNUserNotificationCenter.current().removeAllPendingNotificationRequests() let lastLoopDate = Date() + // Rescheduling reads the last loop date from the shared extension context + var context = ExtensionDataManager.context ?? StatusExtensionContext() + context.lastLoopCompleted = lastLoopDate + ExtensionDataManager.context = context + alertManager.loopDidComplete(lastLoopDate) alertManager.alertMuter.configuration.startTime = Date() alertManager.alertMuter.configuration.duration = .hours(4) waitOnMain() - - let testExpectation = expectation(description: #function) - var loopNotRunningRequests: [UNNotificationRequest] = [] - UNUserNotificationCenter.current().getPendingNotificationRequests() { notificationRequests in - loopNotRunningRequests = notificationRequests.filter({ - $0.content.categoryIdentifier == LoopNotificationCategory.loopNotRunning.rawValue - }) - testExpectation.fulfill() + + var loopNotRunningRequests = pendingLoopNotRunningRequests() + let deadline = Date().addingTimeInterval(5) + while loopNotRunningRequests.contains(where: { $0.content.interruptionLevel == .timeSensitive && $0.content.sound != nil }), Date() < deadline { + RunLoop.current.run(until: Date().addingTimeInterval(0.1)) + loopNotRunningRequests = pendingLoopNotRunningRequests() } - wait(for: [testExpectation], timeout: 1) if #available(iOS 15.0, *) { XCTAssertNil(loopNotRunningRequests.first(where: { $0.content.interruptionLevel == .timeSensitive })?.content.sound) if let request = loopNotRunningRequests.first(where: { $0.content.interruptionLevel == .critical }) { @@ -521,6 +523,19 @@ class AlertManagerTests: XCTestCase { } } } + + private func pendingLoopNotRunningRequests() -> [UNNotificationRequest] { + let pendingExpectation = expectation(description: #function) + var requests: [UNNotificationRequest] = [] + UNUserNotificationCenter.current().getPendingNotificationRequests() { notificationRequests in + requests = notificationRequests.filter({ + $0.content.categoryIdentifier == LoopNotificationCategory.loopNotRunning.rawValue + }) + pendingExpectation.fulfill() + } + wait(for: [pendingExpectation], timeout: 1) + return requests + } } extension Swift.Result { From c795a2d00843444b863bcb0e7b9decbffe198a1f Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Wed, 21 Oct 2026 07:04:33 -0500 Subject: [PATCH 25/26] Leave the Report a Problem options to support plugins (#2595) * Leave the Report a Problem options to support plugins Support had Report Issue, Submit Bug Report (added by LoopSupport) and Export Critical Event Logs next to each other. The ways to report a problem now belong to support plugins; DIY Loop's live in LoopSupport behind a single Report a Problem row. Loop's report form, submitter and local export screen move to LoopSupport, and Loop provides its critical event log export through SupportUIDelegate.criticalEventLogExportView(). * update string catalog --------- Co-authored-by: marionbarker --- Loop.xcodeproj/project.pbxproj | 12 - Loop/Localizable.xcstrings | 3 + Loop/Managers/IssueReportSubmitter.swift | 139 ---------- Loop/Managers/LoopAppManager.swift | 3 +- Loop/Managers/SupportManager.swift | 16 +- .../CommandResponseViewController.swift | 37 --- .../StatusTableViewController.swift | 25 -- Loop/View Models/SettingsViewModel.swift | 37 --- Loop/Views/SendIssueReportView.swift | 254 ------------------ Loop/Views/SettingsView.swift | 12 - 10 files changed, 18 insertions(+), 520 deletions(-) delete mode 100644 Loop/Managers/IssueReportSubmitter.swift delete mode 100644 Loop/View Controllers/CommandResponseViewController.swift delete mode 100644 Loop/Views/SendIssueReportView.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 504223dd3a..f9e627120a 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -54,7 +54,6 @@ 1D4A3E2D2478628500FD601B /* StoredAlert+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4A3E2B2478628500FD601B /* StoredAlert+CoreDataClass.swift */; }; 1D4A3E2E2478628500FD601B /* StoredAlert+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4A3E2C2478628500FD601B /* StoredAlert+CoreDataProperties.swift */; }; 1D63DEA526E950D400F46FA5 /* SupportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D63DEA426E950D400F46FA5 /* SupportManager.swift */; }; - C1A55E012E9F000000A10002 /* IssueReportSubmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A55E012E9F000000A10001 /* IssueReportSubmitter.swift */; }; 1D6B1B6726866D89009AC446 /* AlertPermissionsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D6B1B6626866D89009AC446 /* AlertPermissionsChecker.swift */; }; 1D70C40126EC0F9D00C62570 /* SupportManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D70C40026EC0F9D00C62570 /* SupportManagerTests.swift */; }; 1D80313D24746274002810DF /* AlertStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D80313C24746274002810DF /* AlertStoreTests.swift */; }; @@ -105,7 +104,6 @@ 4328E0351CFC0AE100E199AA /* WatchDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4328E0341CFC0AE100E199AA /* WatchDataManager.swift */; }; 432CF87520D8AC950066B889 /* NSUserDefaults+WatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4328E0231CFBE2C500E199AA /* NSUserDefaults+WatchApp.swift */; }; 432E73CB1D24B3D6009AD15D /* RemoteDataServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 432E73CA1D24B3D6009AD15D /* RemoteDataServicesManager.swift */; }; - 433EA4C41D9F71C800CD78FB /* CommandResponseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 433EA4C31D9F71C800CD78FB /* CommandResponseViewController.swift */; }; 4344628220A7A37F00C4BE6F /* CoreBluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4344628120A7A37E00C4BE6F /* CoreBluetooth.framework */; }; 4344629220A7C19800C4BE6F /* ButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4344629120A7C19800C4BE6F /* ButtonGroup.swift */; }; 4344629820A8B2D700C4BE6F /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4374B5EE209D84BE00D17AA8 /* OSLog.swift */; }; @@ -345,7 +343,6 @@ A987CD4924A58A0100439ADC /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = A987CD4824A58A0100439ADC /* ZipArchive.swift */; }; A999D40624663D18004C89D4 /* PumpManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A999D40524663D18004C89D4 /* PumpManagerError.swift */; }; A9A056B324B93C62007CF06D /* CriticalEventLogExportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A056B224B93C62007CF06D /* CriticalEventLogExportView.swift */; }; - C1A55E012E9F000000A10004 /* SendIssueReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A55E012E9F000000A10003 /* SendIssueReportView.swift */; }; A9A056B524B94123007CF06D /* CriticalEventLogExportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A056B424B94123007CF06D /* CriticalEventLogExportViewModel.swift */; }; A9A63F8E246B271600588D5B /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897341CD2F7DE00223065 /* NSTimeInterval.swift */; }; A9B607B0247F000F00792BE4 /* UserNotifications+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B607AF247F000F00792BE4 /* UserNotifications+Loop.swift */; }; @@ -791,7 +788,6 @@ 1D4A3E2B2478628500FD601B /* StoredAlert+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoredAlert+CoreDataClass.swift"; sourceTree = ""; }; 1D4A3E2C2478628500FD601B /* StoredAlert+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoredAlert+CoreDataProperties.swift"; sourceTree = ""; }; 1D63DEA426E950D400F46FA5 /* SupportManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportManager.swift; sourceTree = ""; }; - C1A55E012E9F000000A10001 /* IssueReportSubmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueReportSubmitter.swift; sourceTree = ""; }; 1D6B1B6626866D89009AC446 /* AlertPermissionsChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPermissionsChecker.swift; sourceTree = ""; }; 1D70C40026EC0F9D00C62570 /* SupportManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportManagerTests.swift; sourceTree = ""; }; 1D80313C24746274002810DF /* AlertStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertStoreTests.swift; sourceTree = ""; }; @@ -844,7 +840,6 @@ 4328E0341CFC0AE100E199AA /* WatchDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = WatchDataManager.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 432E73CA1D24B3D6009AD15D /* RemoteDataServicesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteDataServicesManager.swift; sourceTree = ""; }; 4337615E1D52F487004A3647 /* GlucoseHUDView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseHUDView.swift; sourceTree = ""; }; - 433EA4C31D9F71C800CD78FB /* CommandResponseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommandResponseViewController.swift; sourceTree = ""; }; 4344628120A7A37E00C4BE6F /* CoreBluetooth.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreBluetooth.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk/System/Library/Frameworks/CoreBluetooth.framework; sourceTree = DEVELOPER_DIR; }; 4344628320A7A3BE00C4BE6F /* LoopKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4344628420A7A3BE00C4BE6F /* CGMBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1157,7 +1152,6 @@ A987CD4824A58A0100439ADC /* ZipArchive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipArchive.swift; sourceTree = ""; }; A999D40524663D18004C89D4 /* PumpManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpManagerError.swift; sourceTree = ""; }; A9A056B224B93C62007CF06D /* CriticalEventLogExportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalEventLogExportView.swift; sourceTree = ""; }; - C1A55E012E9F000000A10003 /* SendIssueReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendIssueReportView.swift; sourceTree = ""; }; A9A056B424B94123007CF06D /* CriticalEventLogExportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalEventLogExportViewModel.swift; sourceTree = ""; }; A9B607AF247F000F00792BE4 /* UserNotifications+Loop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserNotifications+Loop.swift"; sourceTree = ""; }; A9B996EF27235191002DC09C /* LoopWarning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopWarning.swift; sourceTree = ""; }; @@ -1956,7 +1950,6 @@ children = ( 43A51E1E1EB6D62A000736CC /* CarbAbsorptionViewController.swift */, 43A51E201EB6DBDD000736CC /* LoopChartsTableViewController.swift */, - 433EA4C31D9F71C800CD78FB /* CommandResponseViewController.swift */, C178249F1E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift */, 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */, 437D9BA21D7BC977007245E8 /* PredictionTableViewController.swift */, @@ -1980,7 +1973,6 @@ 149A28BC2A853E6C00052EDF /* CarbEntryView.swift */, 431A8C3F1EC6E8AB00823B9C /* CircleMaskView.swift */, A9A056B224B93C62007CF06D /* CriticalEventLogExportView.swift */, - C1A55E012E9F000000A10003 /* SendIssueReportView.swift */, C191D2A025B3ACAA00C26C0B /* DosingStrategySelectionView.swift */, 149A28E32A8A63A700052EDF /* FavoriteFoodDetailView.swift */, 142CB75A2A60BFC30075748A /* FavoriteFoodsView.swift */, @@ -2038,7 +2030,6 @@ B470F5832AB22B5100049695 /* StatefulPluggable.swift */, 43FCEEA8221A615B0013DD30 /* StatusChartsManager.swift */, 1D63DEA426E950D400F46FA5 /* SupportManager.swift */, - C1A55E012E9F000000A10001 /* IssueReportSubmitter.swift */, 4F70C20F1DE8FAC5006380B7 /* ExtensionDataManager.swift */, 89ADE13A226BFA0F0067222B /* TestingScenariosManager.swift */, 1D82E69F25377C6B009131FB /* TrustedTimeChecker.swift */, @@ -3395,7 +3386,6 @@ 897A5A9624C2175B00C4E71D /* BolusEntryView.swift in Sources */, 4F70C2131DE90339006380B7 /* StatusExtensionContext.swift in Sources */, A9A056B324B93C62007CF06D /* CriticalEventLogExportView.swift in Sources */, - C1A55E012E9F000000A10004 /* SendIssueReportView.swift in Sources */, 43C05CC521EC29E3006FB252 /* TextFieldTableViewCell.swift in Sources */, 4FF4D1001E18374700846527 /* WatchContext.swift in Sources */, C1D289B522F90A52003FFBD9 /* BasalDeliveryState.swift in Sources */, @@ -3516,7 +3506,6 @@ A96DAC2C2838F31200D94E38 /* SharedLogging.swift in Sources */, 4302F4E31D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift in Sources */, 1D63DEA526E950D400F46FA5 /* SupportManager.swift in Sources */, - C1A55E012E9F000000A10002 /* IssueReportSubmitter.swift in Sources */, 4FC8C8011DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift in Sources */, 43E93FB61E469A4000EAB8DB /* NumberFormatter.swift in Sources */, C1FB428C217806A400FAB378 /* StateColorPalette.swift in Sources */, @@ -3541,7 +3530,6 @@ A987CD4924A58A0100439ADC /* ZipArchive.swift in Sources */, 43F41C371D3BF32400C11ED6 /* UIAlertController.swift in Sources */, A9CBE45C248ACC03008E7BA2 /* SettingsStore+SimulatedCoreData.swift in Sources */, - 433EA4C41D9F71C800CD78FB /* CommandResponseViewController.swift in Sources */, E9B3552229358C440076AB04 /* MealDetectionManager.swift in Sources */, C16DA84222E8E112008624C2 /* PluginManager.swift in Sources */, 43785E932120A01B0057DED1 /* NewCarbEntryIntent+Loop.swift in Sources */, diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 217ff2266f..afb02be490 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -19028,6 +19028,7 @@ }, "Export Critical Event Logs" : { "comment" : "The title of the export critical event logs in support", + "extractionState" : "stale", "localizations" : { "da" : { "stringUnit" : { @@ -24168,6 +24169,7 @@ }, "Issue Report" : { "comment" : "The title text for the issue report menu item\nThe view controller title for the issue report screen", + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -24804,6 +24806,7 @@ }, "Loading..." : { "comment" : "The loading message for the diagnostic report screen", + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { diff --git a/Loop/Managers/IssueReportSubmitter.swift b/Loop/Managers/IssueReportSubmitter.swift deleted file mode 100644 index 68669dd2f3..0000000000 --- a/Loop/Managers/IssueReportSubmitter.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// IssueReportSubmitter.swift -// Loop -// -// Copyright © 2026 LoopKit Authors. All rights reserved. -// - -import Foundation - -struct IssueReport { - struct Contact: Encodable { - var name: String? - var email: String? - var zulip: String? - var github: String? - } - - struct Attachment { - var filename: String - var contentType: String - var data: Data - } - - var title: String? - var description: String - var contact: Contact? - var context: [String: IssueReportContextValue] - var attachments: [Attachment] -} - -enum IssueReportContextValue: Encodable, Equatable { - case string(String) - case strings([String]) - - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .string(let value): try container.encode(value) - case .strings(let value): try container.encode(value) - } - } -} - -enum IssueReportSubmitterError: LocalizedError { - case server(status: Int, reason: String?) - case invalidResponse - case network(Error) - - var isRetryable: Bool { - switch self { - case .server(let status, _): return status >= 500 - case .invalidResponse, .network: return true - } - } - - var errorDescription: String? { - switch self { - case .server(413, _): - return NSLocalizedString("The report is too large. Try fewer or smaller screenshots.", comment: "Issue report error when the upload is too large") - case .server(415, _): - return NSLocalizedString("One of the attachments isn't a supported image type.", comment: "Issue report error when an attachment type is refused") - case .server(429, _): - return NSLocalizedString("Too many reports have been sent from this network recently. Please try again later.", comment: "Issue report error when rate limited") - case .server(let status, let reason): - return String(format: NSLocalizedString("The server couldn't accept the report (%1$d %2$@).", comment: "Issue report error with HTTP status (1) and reason (2)"), status, reason ?? "") - case .invalidResponse: - return NSLocalizedString("The server sent an unexpected response.", comment: "Issue report error for an unreadable response") - case .network(let error): - return error.localizedDescription - } - } -} - -struct IssueReportSubmitter { - static let endpoint = URL(string: "https://ingest.loopkit.org/v1/issues")! - static let apiKey = "7dd7414785560c0dd1ef802ac10c00b4" - - var session: URLSession = .shared - - func submit(_ report: IssueReport) async throws -> String { - let boundary = "Boundary-\(UUID().uuidString)" - var request = URLRequest(url: Self.endpoint) - request.httpMethod = "POST" - request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") - - let body = try Self.body(for: report, boundary: boundary) - let data: Data - let response: URLResponse - do { - (data, response) = try await session.upload(for: request, from: body) - } catch { - throw IssueReportSubmitterError.network(error) - } - guard let response = response as? HTTPURLResponse else { - throw IssueReportSubmitterError.invalidResponse - } - let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] - guard response.statusCode == 200 else { - throw IssueReportSubmitterError.server(status: response.statusCode, reason: json?["error"] as? String) - } - guard let id = json?["id"] as? String else { - throw IssueReportSubmitterError.invalidResponse - } - return id - } - - static func body(for report: IssueReport, boundary: String) throws -> Data { - struct Payload: Encodable { - var title: String? - var description: String - var contact: IssueReport.Contact? - var context: [String: IssueReportContextValue] - } - let payload = try JSONEncoder().encode(Payload(title: report.title, description: report.description, contact: report.contact, context: report.context)) - - var body = Data() - func appendPart(name: String, filename: String? = nil, contentType: String? = nil, data: Data) { - var header = "--\(boundary)\r\nContent-Disposition: form-data; name=\"\(name)\"" - if let filename { - header += "; filename=\"\(filename.replacingOccurrences(of: "\"", with: ""))\"" - } - header += "\r\n" - if let contentType { - header += "Content-Type: \(contentType)\r\n" - } - body.append(Data((header + "\r\n").utf8)) - body.append(data) - body.append(Data("\r\n".utf8)) - } - - appendPart(name: "client", data: Data(apiKey.utf8)) - appendPart(name: "report", contentType: "application/json", data: payload) - for attachment in report.attachments { - appendPart(name: "attachment", filename: attachment.filename, contentType: attachment.contentType, data: attachment.data) - } - body.append(Data("--\(boundary)--\r\n".utf8)) - return body - } -} diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index b9a36d1295..efe8d2ad67 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -228,7 +228,8 @@ class LoopAppManager: NSObject { supportManager = SupportManager(pluginManager: pluginManager, deviceSupportDelegate: deviceDataManager, servicesManager: deviceDataManager.servicesManager, - alertIssuer: alertManager) + alertIssuer: alertManager, + criticalEventLogExporterFactory: deviceDataManager.criticalEventLogExportManager) setWhitelistedDevices() diff --git a/Loop/Managers/SupportManager.swift b/Loop/Managers/SupportManager.swift index 18f3df912d..eb6a066874 100644 --- a/Loop/Managers/SupportManager.swift +++ b/Loop/Managers/SupportManager.swift @@ -40,6 +40,7 @@ public final class SupportManager { private let pluginManager: PluginManager private let staticSupportTypes: [SupportUI.Type] private let staticSupportTypesByIdentifier: [String: SupportUI.Type] + private let criticalEventLogExporterFactory: CriticalEventLogExporterFactory? lazy private var cancellables = Set() @@ -47,11 +48,13 @@ public final class SupportManager { deviceSupportDelegate: DeviceSupportDelegate, servicesManager: ServicesManager? = nil, staticSupportTypes: [SupportUI.Type]? = nil, - alertIssuer: AlertIssuer) { - + alertIssuer: AlertIssuer, + criticalEventLogExporterFactory: CriticalEventLogExporterFactory? = nil) { + self.alertIssuer = alertIssuer self.deviceSupportDelegate = deviceSupportDelegate self.pluginManager = pluginManager + self.criticalEventLogExporterFactory = criticalEventLogExporterFactory self.staticSupportTypes = [] staticSupportTypesByIdentifier = self.staticSupportTypes.reduce(into: [:]) { (map, type) in map[type.pluginIdentifier] = type @@ -217,7 +220,14 @@ extension SupportManager: SupportUIDelegate { public func openURL(url: URL) { UIApplication.shared.open(url) } - + + public func criticalEventLogExportView() -> AnyView { + guard let criticalEventLogExporterFactory else { + return AnyView(EmptyView()) + } + return AnyView(CriticalEventLogExportView(viewModel: CriticalEventLogExportViewModel(exporterFactory: criticalEventLogExporterFactory))) + } + public var pumpStatus: LoopKit.PumpManagerStatus? { deviceSupportDelegate.pumpManagerStatus } diff --git a/Loop/View Controllers/CommandResponseViewController.swift b/Loop/View Controllers/CommandResponseViewController.swift deleted file mode 100644 index e14c41c8a4..0000000000 --- a/Loop/View Controllers/CommandResponseViewController.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// CommandResponseViewController.swift -// Loop -// -// Created by Nate Racklyeft on 9/30/16. -// Copyright © 2016 LoopKit Authors. All rights reserved. -// - -import Foundation -import LoopKitUI - - -extension CommandResponseViewController { - typealias T = CommandResponseViewController - - static func generateDiagnosticReport(deviceManager: DeviceDataManager) -> T { - let date = Date() - let vc = T(command: { (completionHandler) in - deviceManager.generateDiagnosticReport { (report) in - DispatchQueue.main.async { - completionHandler([ - "Use the Share button above to save this diagnostic report to aid investigating your problem. Issues can be filed at https://github.com/LoopKit/Loop/issues.", - "Generated: \(date)", - "", - report, - "", - ].joined(separator: "\n\n")) - } - } - - return NSLocalizedString("Loading...", comment: "The loading message for the diagnostic report screen") - }) - vc.fileName = "Loop Report \(ISO8601DateFormatter.string(from: date, timeZone: .current, formatOptions: [.withSpaceBetweenDateAndTime, .withInternetDateTime])).md" - - return vc - } -} diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index eb91cc21d1..580d67eb8a 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -1628,7 +1628,6 @@ final class StatusTableViewController: LoopChartsTableViewController { pumpManagerSettingsViewModel: pumpViewModel, cgmManagerSettingsViewModel: cgmViewModel, servicesViewModel: servicesViewModel, - criticalEventLogExportViewModel: CriticalEventLogExportViewModel(exporterFactory: deviceManager.criticalEventLogExportManager), therapySettings: { [weak self] in self?.deviceManager.loopManager.therapySettings ?? TherapySettings() }, sensitivityOverridesEnabled: FeatureFlags.sensitivityOverridesEnabled, initialDosingEnabled: deviceManager.loopManager.settings.dosingEnabled, @@ -2224,30 +2223,6 @@ extension StatusTableViewController: SettingsViewModelDelegate { settings.automaticDosingStrategy = strategy } } - - func didTapIssueReport() { - // TODO: this dismiss here is temporary, until we know exactly where - // we want this screen to belong in the navigation flow - dismiss(animated: true) { - let vc = CommandResponseViewController.generateDiagnosticReport(deviceManager: self.deviceManager) - vc.title = NSLocalizedString("Issue Report", comment: "The view controller title for the issue report screen") - self.show(vc, sender: nil) - } - } - - func generateDiagnosticReport() async -> String { - await withCheckedContinuation { continuation in - deviceManager.generateDiagnosticReport { continuation.resume(returning: $0) } - } - } - - var pumpModel: String? { - deviceManager.pumpManager.map { $0.status.device.model ?? "Unknown" } - } - - var cgmModel: String? { - deviceManager.cgmManager.map { $0.cgmManagerStatus.device?.model ?? "Unknown" } - } } // MARK: - Services delegation diff --git a/Loop/View Models/SettingsViewModel.swift b/Loop/View Models/SettingsViewModel.swift index 672dfa449e..4ce2c1b12f 100644 --- a/Loop/View Models/SettingsViewModel.swift +++ b/Loop/View Models/SettingsViewModel.swift @@ -52,11 +52,7 @@ public typealias PumpManagerViewModel = DeviceViewModel public protocol SettingsViewModelDelegate: AnyObject { func dosingEnabledChanged(_: Bool) func dosingStrategyChanged(_: AutomaticDosingStrategy) - func didTapIssueReport() - func generateDiagnosticReport() async -> String var closedLoopDescriptiveText: String? { get } - var pumpModel: String? { get } - var cgmModel: String? { get } } public class SettingsViewModel: ObservableObject { @@ -69,40 +65,10 @@ public class SettingsViewModel: ObservableObject { private weak var delegate: SettingsViewModelDelegate? - func didTapIssueReport() { - delegate?.didTapIssueReport() - } - - var generateDiagnosticReport: (() async -> String)? { - guard let delegate else { - return nil - } - return { [weak delegate] in await delegate?.generateDiagnosticReport() ?? "" } - } - - @MainActor - var issueReportContext: [String: IssueReportContextValue] { - let buildDetails = BuildDetails.default - var context: [String: IssueReportContextValue] = [ - "appVersion": .string(Bundle.main.shortVersionString), - "buildNumber": .string(Bundle.main.version), - "customizations": .strings(buildDetails.customizations), - "deviceModel": .string(UIDevice.current.modelIdentifier), - "osVersion": .string(UIDevice.current.systemVersion), - ] - context["branch"] = (buildDetails.workspaceGitBranch ?? buildDetails.gitBranch).map { .string($0) } - context["workspaceRevision"] = buildDetails.workspaceGitRevision.map { .string($0) } - context["loopRevision"] = buildDetails.gitRevision.map { .string($0) } - context["pumpModel"] = delegate?.pumpModel.map { .string($0) } - context["cgmModel"] = delegate?.cgmModel.map { .string($0) } - return context - } - var availableSupports: [SupportUI] let pumpManagerSettingsViewModel: PumpManagerViewModel let cgmManagerSettingsViewModel: CGMManagerViewModel let servicesViewModel: ServicesViewModel - let criticalEventLogExportViewModel: CriticalEventLogExportViewModel let therapySettings: () -> TherapySettings let sensitivityOverridesEnabled: Bool let isOnboardingComplete: Bool @@ -139,7 +105,6 @@ public class SettingsViewModel: ObservableObject { pumpManagerSettingsViewModel: PumpManagerViewModel, cgmManagerSettingsViewModel: CGMManagerViewModel, servicesViewModel: ServicesViewModel, - criticalEventLogExportViewModel: CriticalEventLogExportViewModel, therapySettings: @escaping () -> TherapySettings, sensitivityOverridesEnabled: Bool, initialDosingEnabled: Bool, @@ -156,7 +121,6 @@ public class SettingsViewModel: ObservableObject { self.pumpManagerSettingsViewModel = pumpManagerSettingsViewModel self.cgmManagerSettingsViewModel = cgmManagerSettingsViewModel self.servicesViewModel = servicesViewModel - self.criticalEventLogExportViewModel = criticalEventLogExportViewModel self.therapySettings = therapySettings self.sensitivityOverridesEnabled = sensitivityOverridesEnabled self.closedLoopPreference = initialDosingEnabled @@ -204,7 +168,6 @@ extension SettingsViewModel { pumpManagerSettingsViewModel: DeviceViewModel(), cgmManagerSettingsViewModel: DeviceViewModel(), servicesViewModel: ServicesViewModel.preview, - criticalEventLogExportViewModel: CriticalEventLogExportViewModel(exporterFactory: MockCriticalEventLogExporterFactory()), therapySettings: { TherapySettings() }, sensitivityOverridesEnabled: false, initialDosingEnabled: true, diff --git a/Loop/Views/SendIssueReportView.swift b/Loop/Views/SendIssueReportView.swift deleted file mode 100644 index 5eb2ca73ee..0000000000 --- a/Loop/Views/SendIssueReportView.swift +++ /dev/null @@ -1,254 +0,0 @@ -// -// SendIssueReportView.swift -// Loop -// -// Copyright © 2026 LoopKit Authors. All rights reserved. -// - -import PhotosUI -import SwiftUI - -struct SendIssueReportView: View { - let context: [String: IssueReportContextValue] - let generateDiagnosticReport: (() async -> String)? - let exportLocally: () -> Void - - @Environment(\.dismiss) private var dismiss - - @State private var title = "" - @State private var description = "" - @State private var isAnonymous = true - @State private var contact = IssueReport.Contact() - @State private var includeDiagnosticReport = true - @State private var photoSelection: [PhotosPickerItem] = [] - @State private var screenshots: [Screenshot] = [] - @State private var isSending = false - @State private var sentReportID: String? - @State private var sendError: IssueReportSubmitterError? - @State private var showingError = false - - private static let maxDescriptionLength = 20_000 - private static let maxScreenshots = 8 - private static let maxAttachmentBytes = 10_000_000 - - private struct Screenshot: Identifiable { - let id = UUID() - let image: UIImage - let attachment: IssueReport.Attachment - } - - var body: some View { - Form { - Section(footer: Text(String(format: NSLocalizedString("%1$d of %2$d characters", comment: "Issue report description length (1: used)(2: limit)"), description.count, Self.maxDescriptionLength))) { - TextField(NSLocalizedString("Title (optional)", comment: "Placeholder for the issue report title"), text: $title) - ZStack(alignment: .topLeading) { - if description.isEmpty { - Text(NSLocalizedString("Describe what you saw and what you expected to happen", comment: "Placeholder for the issue report description")) - .foregroundColor(Color(.placeholderText)) - .padding(.top, 8) - .padding(.leading, 4) - } - TextEditor(text: $description) - .frame(minHeight: 160) - } - } - - screenshotsSection - - Section(footer: Text(isAnonymous ? - NSLocalizedString("The report won't include your name, contact details or anything else that identifies you.", comment: "Footer for an anonymous issue report") : - NSLocalizedString("Fill in any of these so developers can follow up with you.", comment: "Footer for issue report contact details")) - ) { - Toggle(NSLocalizedString("Submit Anonymously", comment: "Toggle for submitting an issue report without contact details"), isOn: $isAnonymous) - if !isAnonymous { - TextField(NSLocalizedString("Name", comment: "Issue report contact name placeholder"), text: binding(\.name)) - .textContentType(.name) - TextField(NSLocalizedString("Email", comment: "Issue report contact email placeholder"), text: binding(\.email)) - .textContentType(.emailAddress) - .keyboardType(.emailAddress) - .textInputAutocapitalization(.never) - TextField(NSLocalizedString("Zulip Name", comment: "Issue report contact Zulip placeholder"), text: binding(\.zulip)) - .textInputAutocapitalization(.never) - TextField(NSLocalizedString("GitHub Username", comment: "Issue report contact GitHub placeholder"), text: binding(\.github)) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() - } - } - - if generateDiagnosticReport != nil { - Section(footer: Text(NSLocalizedString("Attaches Loop's issue report as a text file. It contains health data such as recent glucose, insulin, carbs and your therapy settings.", comment: "Footer for the include diagnostic report toggle"))) { - Toggle(NSLocalizedString("Include Diagnostic Report", comment: "Toggle for attaching the diagnostic report to an issue report"), isOn: $includeDiagnosticReport) - } - } - - Section(header: Text(NSLocalizedString("Also Sent", comment: "Header for the build and device details sent with an issue report")), - footer: Text(NSLocalizedString("Reports go to the LoopKit development team and are visible only to them. Your IP address is used only to limit how often reports can be sent and is never stored.", comment: "Privacy footer for issue reports"))) - { - ForEach(context.keys.sorted(), id: \.self) { key in - HStack { - Text(key) - Spacer() - Text(Self.displayValue(context[key])) - .foregroundColor(.secondary) - .multilineTextAlignment(.trailing) - } - .font(.footnote) - } - } - - Section(footer: Text(NSLocalizedString("Opens Loop's diagnostic report so you can save it or share it yourself. Nothing is sent to the developers.", comment: "Footer for exporting the issue report locally"))) { - Button(NSLocalizedString("Export Locally Instead", comment: "Button to open the diagnostic report for local export instead of submitting"), action: exportLocally) - } - } - .navigationTitle(NSLocalizedString("Report Issue", comment: "Title of the issue report screen")) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .confirmationAction) { - if isSending { - ProgressView() - } else { - Button(NSLocalizedString("Submit", comment: "Button to submit an issue report to the developers")) { - send() - } - .disabled(!canSend) - } - } - } - .disabled(isSending) - .onChange(of: photoSelection) { _, items in - loadScreenshots(items) - } - .alert(NSLocalizedString("Report Submitted", comment: "Title of the alert after an issue report was submitted"), isPresented: Binding(get: { sentReportID != nil }, set: { if !$0 { sentReportID = nil } })) { - Button(NSLocalizedString("Copy ID", comment: "Button to copy the issue report ID")) { - UIPasteboard.general.string = sentReportID - dismiss() - } - Button(NSLocalizedString("Done", comment: "Button to close the issue report sent alert"), role: .cancel) { - dismiss() - } - } message: { - Text(String(format: NSLocalizedString("Thank you. Your report ID is %1$@. Mention it if you discuss the problem on Zulip or GitHub.", comment: "Message after an issue report was sent (1: report ID)"), sentReportID ?? "")) - } - .alert(NSLocalizedString("Unable to Submit Report", comment: "Title of the alert when an issue report fails"), isPresented: $showingError, presenting: sendError) { error in - if error.isRetryable { - Button(NSLocalizedString("Try Again", comment: "Button to retry sending an issue report")) { - send() - } - } - Button(NSLocalizedString("OK", comment: "Button to close the issue report error alert"), role: .cancel) {} - } message: { error in - Text(error.localizedDescription) - } - } - - private var screenshotsSection: some View { - Section(footer: Text(String(format: NSLocalizedString("Up to %1$d screenshots or photos.", comment: "Footer for issue report screenshots (1: limit)"), Self.maxScreenshots))) { - if !screenshots.isEmpty { - ScrollView(.horizontal) { - HStack { - ForEach(screenshots) { screenshot in - Image(uiImage: screenshot.image) - .resizable() - .scaledToFit() - .frame(height: 120) - .clipShape(RoundedRectangle(cornerRadius: 6)) - } - } - } - } - PhotosPicker(selection: $photoSelection, maxSelectionCount: Self.maxScreenshots, matching: .images) { - Text(screenshots.isEmpty ? - NSLocalizedString("Add Screenshots", comment: "Button to choose screenshots for an issue report") : - NSLocalizedString("Change Screenshots", comment: "Button to change the screenshots on an issue report")) - } - } - } - - private var trimmedDescription: String { - description.trimmingCharacters(in: .whitespacesAndNewlines) - } - - private var canSend: Bool { - !trimmedDescription.isEmpty && description.count <= Self.maxDescriptionLength - } - - private func binding(_ keyPath: WritableKeyPath) -> Binding { - Binding(get: { contact[keyPath: keyPath] ?? "" }, set: { contact[keyPath: keyPath] = $0 }) - } - - private func loadScreenshots(_ items: [PhotosPickerItem]) { - Task { - var loaded: [Screenshot] = [] - for (index, item) in items.enumerated() { - guard let data = try? await item.loadTransferable(type: Data.self), let screenshot = Self.screenshot(from: data, index: index + 1) else { - continue - } - loaded.append(screenshot) - } - screenshots = loaded - } - } - - // The server accepts only PNG and JPEG, so anything else (HEIC from the camera) is converted - private static func screenshot(from data: Data, index: Int) -> Screenshot? { - guard let image = UIImage(data: data) else { - return nil - } - if data.starts(with: [0x89, 0x50, 0x4E, 0x47]), data.count <= maxAttachmentBytes { - return Screenshot(image: image, attachment: .init(filename: "screenshot-\(index).png", contentType: "image/png", data: data)) - } - for quality in [0.8, 0.5, 0.3] { - if let jpeg = image.jpegData(compressionQuality: quality), jpeg.count <= maxAttachmentBytes { - return Screenshot(image: image, attachment: .init(filename: "screenshot-\(index).jpg", contentType: "image/jpeg", data: jpeg)) - } - } - return nil - } - - private static func displayValue(_ value: IssueReportContextValue?) -> String { - switch value { - case .string(let string): return string - case .strings(let strings): return strings.isEmpty ? "–" : strings.joined(separator: ", ") - case nil: return "" - } - } - - private func send() { - isSending = true - Task { - var attachments = screenshots.map(\.attachment) - if includeDiagnosticReport, let generateDiagnosticReport { - let report = await generateDiagnosticReport() - // Stay under the server's per-file limit; decoding repairs a character cut in half - let text = String(decoding: report.utf8.prefix(Self.maxAttachmentBytes - 4), as: UTF8.self) - attachments.append(.init(filename: "diagnostic-report.txt", contentType: "text/plain; charset=utf-8", data: Data(text.utf8))) - } - let report = IssueReport( - title: title.isEmpty ? nil : String(title.prefix(200)), - description: trimmedDescription, - contact: isAnonymous ? nil : Self.trimmed(contact), - context: context, - attachments: attachments - ) - do { - sentReportID = try await IssueReportSubmitter().submit(report) - } catch let error as IssueReportSubmitterError { - sendError = error - showingError = true - } catch { - sendError = .network(error) - showingError = true - } - isSending = false - } - } - - private static func trimmed(_ contact: IssueReport.Contact) -> IssueReport.Contact? { - func clean(_ value: String?) -> String? { - let value = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - return value.isEmpty ? nil : value - } - let result = IssueReport.Contact(name: clean(contact.name), email: clean(contact.email), zulip: clean(contact.zulip), github: clean(contact.github)) - return [result.name, result.email, result.zulip, result.github].allSatisfy({ $0 == nil }) ? nil : result - } -} diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index b0f2567f18..9f9e3aa069 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -457,21 +457,9 @@ extension SettingsView { private var supportSection: some View { Section(header: SectionHeader(label: NSLocalizedString("Support", comment: "The title of the support section in settings"))) { - NavigationLink(destination: SendIssueReportView( - context: viewModel.issueReportContext, - generateDiagnosticReport: viewModel.generateDiagnosticReport, - exportLocally: viewModel.didTapIssueReport - )) { - Text(NSLocalizedString("Report Issue", comment: "The title text for the issue report menu item")) - } - ForEach(pluginMenuItems.filter( { $0.section == .support })) { $0.view } - - NavigationLink(destination: CriticalEventLogExportView(viewModel: viewModel.criticalEventLogExportViewModel)) { - Text(NSLocalizedString("Export Critical Event Logs", comment: "The title of the export critical event logs in support")) - } } } From da7007a6999c2b0f573559fa62db0377fae587a6 Mon Sep 17 00:00:00 2001 From: Marion Barker <19607791+marionbarker@users.noreply.github.com> Date: Thu, 22 Oct 2026 14:16:53 -0700 Subject: [PATCH 26/26] Updated translations from lokalise on Thu Oct 22 11:50:14 PDT 2026 (#2598) --- Loop/Localizable.xcstrings | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index afb02be490..738c2b1e6a 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -35457,6 +35457,12 @@ "value" : "Koolhydraatinvoer vereenvoudigen" } }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Simplificați introducerea carbohidraților" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated",